60 lines
38 KiB
Markdown
60 lines
38 KiB
Markdown
# Catatan mesin (untuk yang mengerjakan fitur)
|
||
|
||
Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks untuk pemain dalam **bahasa Indonesia**. Dibangun dari AETHER CLASH (Bang Tutorial).
|
||
|
||
## Peta berkas
|
||
- `index.html` memuat semua script berurutan: manifest tiap karakter → `announcer.js` → `match.js` → `feel.js` → `game.js` → `touch.js` → `menu.js` → `meta.js`. Gaya: `style.css` (arena/HUD), `feel.css` (peringkat kombo), `menu.css` (menu), `meta.css` (toko, karakter, koleksi, toast, tombol sentuh tambahan).
|
||
- Urutan muat terkini dan titik temu fitur Tahap 3 (hasil integrasi `plan/t3`). Skrip: `brand.js` → `content/story.js` → `story.js` (blok awal, sebelum `precache.js`) ... `feel.js` → `content/passives.js` → `passives.js` → `content/quotes.js` → `content/dialogs.js` → `vs.js` → `game.js` → `touch.js` → `content/mystery.js` → `mystery.js` → `menu.js` → `meta.js`; gaya: `style.css`, `menu.css`, `meta.css`, `feel.css`, `vs.css`, `story.css`, `mystery.css`. Titik temu: (1) `startMatch` memanggil `refreshPassives(matchStyles(...))` lalu `setShadowFoe(...)`, dan info layar VS membawa `styles` (dipakai `Passives.decorateVS`) sekaligus `shadow` (dipakai `Story.decorateVs`). (2) `VS.show` menyusun dialog atau kutipan, lalu `Passives.decorateVS` menambah baris `.vs-perk` di AKHIR `.vs-info` (sesudah balon, supaya ekor balon tetap menunjuk papan nama); untuk Bayangan `Story.decorateVs` hanya mengganti teks `.vs-who`, `.vs-name`, `.vs-tag`, `.vs-quote` sisi kanan sehingga baris pasif tetap ada. (3) Bayangan memakai id yang sama dengan pemain, jadi `VS.dialog` mengembalikan null dan kutipan umum dipakai; CPU Bayangan mendapat gaya acak seperti CPU lain (`styleRand`), bukan gaya pemain. (4) Urutan pengali damage: `passiveDamage` dulu, lalu `foeDamage` (+10% Bayangan) di pembungkus `__game.receiveHit` dan `dotDamage`. (5) `Meta.toast` menahan toast selama `VS.active` atau `Story.active`, jadi toast `Meta.pairEgg` (dipicu dari `VS.show`) muncul sesudah layar VS. (6) `mystery.js` menghitung `eggs` tanpa kunci berawalan `code:` dan `dialog:`, dan `pairs` dari `dialog:*`; pencapaian `reuni` dan `penasaran_berat` sama-sama tersembunyi di Koleksi.
|
||
- `game.js` (~2400 baris, satu IIFE): seluruh mesin. Pemain = `hero`, lawan = `dummy` (CPU atau P2). Fungsi penting:
|
||
- `step(dt)` loop logika; `draw()` menggambar (`drawBackground`, `drawHero`, `drawDummy`, `drawEffects`, `drawCfBack/Front`, dst); `updateHud()`/`updateMatchHud()` mengisi HUD DOM.
|
||
- Serangan: `startAttack`, `cast`/`castMove`, `meleeHit`, `fireSkill`, `hitDummy` (pemain memukul lawan), `window.__game.receiveHit` (lawan memukul pemain), `startCommand`/`specialHit`/`startThrow`/`useShopMove`/`heroGuard` (gerakan perintah, lempar, blok/parry, jurus toko).
|
||
- Karakter data-driven: `window.FIGHTERS[id]` (dari `assets/<id>/manifest.js`) → `cfKit(d)` (gerakan), `cfStrike(actor, def, move)` (efek pukul: jenis projectile/wave/homing/dash/area/pillar/lob; ultimate volley/stampede/rain/pillars via `updateCfUltimate`). Bro Wo ditulis tangan (`browo.js` + `updateBrowo`, `browoStrike`).
|
||
- Proyektil: array `projectiles`, diperbarui di `updateProjectiles(dt)`; efek visual: array `effects` (tipe `slash`, `impact`, `cf-fx`, `cf-pillar`, ...) digambar di `drawEffects()`; partikel `spawnParticles`; angka damage `damageNumbers`.
|
||
- Efek jurus (blok "Efek jurus (FX)" di game.js, dipakai pemain dan CPU lewat `cfStrike`): proyektil CF punya umur (`p.age`) dan gaya gambar `p.fx` (rings, shout, chain, roll, spray, flame, bull, drive, wave, bolt, ant); `fxPart` membuat partikel kosmetik (pengacak terpisah `fxr()`, bukan `random()`); jurus area punya aba-aba cincin 0,25 s (`fxTeleP`), gelombang kejut dan retakan; dash meninggalkan bayangan sprite (`cfx.ghosts`). Flag data di manifest per skill (juga `mode.skills`): `fx`, `lift`, `trail`, `split`, `dot`, `bind`, `convoy`, `aftershock`, `pull`, `rebound`, `swarm`+`ramp`, `fire`; artinya ada di komentar "Mekanik jurus" di game.js.
|
||
- Rasa kena (`feel.js`, `window.Feel`): tabel penyetelan `Feel.table` per jenis serangan (ringan/sedang/berat/ultimate/guard/parry/counter: hit-stop, getaran, sentakan layar, kilat putih, ukuran angka, riak), `Feel.classify(damage, {counter, shake})`, K.O. slow-motion (`Feel.ko`, skala hanya untuk dt simulasi visual; jam ronde tetap waktu nyata), angka damage pop (maks 8) dan peringkat kombo `#combo-rank`. Titik pakai di game.js: `feelImpact` (satu pintu efek), `feelHeroHit` (CPU memukul pemain), `hitDummy` (pemain memukul lawan), `startKoFx`.
|
||
- CPU: `updateFenrAI`, `cpuProfile` (tingkat easy/medium/hard/excellent dari `match.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']`.
|
||
- Pasif, kelemahan, dan gaya bertarung (RENCANA 3.2/3.3): data di `content/passives.js` (`window.PASSIVES[id] = {name, desc, flaw, mods}`, `window.STYLES` agresif/seimbang/bertahan), helper dan teks angka otomatis di `passives.js` (`window.Passives`: `merge`, `describe`, `lockerHtml`, `decorateVS`). Mesin: `refreshPassives(styles)` di `startMatch` (juga `selectCharacter`, `setP2`) menggabungkan perk + kelemahan + gaya per sisi ke `sideMods`; semua pembacaan lewat satu titik kait `passiveMod(actor, key, base)` (hero = sisi pemain; selain itu = lawan, CPU atau P2, jadi simetris). Kunci: `dmgOut`, `dmgBasic`, `dmgSkill`, `dmgIn`, `walk`, `cd`, `cdSkill`, `cdUlt`, `parry`, `modeDur` (pengali) dan `regen` (HP/dtk, penambah). Damage dikali di `hitDummy`/`receiveHit`/`dotDamage` lewat `passiveDamage` (dibulatkan dengan sisa yang dibawa, tanpa pengacak); cooldown di loop cooldown `step`/`updateDummy`; jalan di `step`/`updateP2`/`updateFenrAI`; parry di `press('l')` dan peluang blok CPU di `hitDummy`. Gaya pemain disimpan di `Meta.save.style[id]` (`Meta.styleOf/setStyle`, simpanan lama tanpa kunci = Seimbang); `startMatch({styles:{player,enemy}})` menimpa, dan CPU di pertandingan ber-VS memakai gaya acak (pengacak terpisah `styleRand`). Teks tampil di menu Karakter (`renderLocker`), layar VS (`.vs-perk`), dan Daftar gerakan (`passiveRows`). API uji: `__game.passives()`, `__game.passiveMod(actor,key,base)`, `__game.setStyles({player,enemy})`. Catatan: P2 manusia tidak punya tombol blok, jadi kunci `parry` tidak berefek bagi P2.
|
||
- `menu.js`: menu utama (home, pilih karakter, arena, hasil, about) di `#front-menu`; `window.FrontEnd`.
|
||
- Latar hidup dan cuaca (RENCANA 6.1 dan 6.3): `arena.js` (`window.Arena`) + `arena.css`; tagnya di baris sendiri sebelum `</main>` di `index.html` (urutan muat bebas: kait dipasang saat DOMContentLoaded). Kait: `game.js` `draw()` memanggil `Arena.back(ctx, {viewW, viewH, scale, offsetX, offsetY, shakeX, shakeY, groundY, stage, time: realTime, menu, combo: comboHits, cin: cinematic})` tepat sesudah `drawBackground` dan `Arena.front(ctx)` sebagai pernyataan terakhir; `menu.js` memanggil `Arena.decorate(root, screen)` di akhir `render()` dan mengirim `weather: Arena.pick` di `startMatch`. Pembungkus `__game.startMatch` (dipasang `arena.js`) mengingat `options.weather` hanya bila pertandingan benar-benar mulai; tanpa opsi (Arcade, Survival, tutorial, uji) cuaca = Otomatis, yaitu `ARENA[stage].auto` (semua cerah kecuali Elderwood kabut tipis dan Moonrise malam; arena yang belum ada di tabel memakai entri `_`). Cuaca (hanya tampilan, tidak menyentuh mekanik): Cerah, Hujan, Kabut, Senja, Malam; preset di `WX`. Semua gerak adalah fungsi waktu tanpa status (hash bilangan bulat `rnd(salt, i, k)`, bukan `random()` mesin dan bukan `Date`), jadi `advance()` yang melompat jauh, jeda (realTime berhenti), dan hasil pertandingan aman. Lapisan belakang (kanvas, antara latar dan petarung): tint pengali, awan (memudar di kotak `keep` bangunan karena digambar di atas gambar), burung/camar/kelelawar, kilau air dan perahu (Harbor, sungai Moonrise), debu cahaya dan berkas cahaya (Sunspire), daun dan kelopak (Elderwood), kunang-kunang, bintang, bintang jatuh (malam), hujan jauh dan percikan lantai, kabut, sorotan lantai. Lapisan depan (kanvas, layar penuh, tidak ikut zoom kamera): siluet penonton di tepi bawah (semangat `C.ex` naik saat kombo 5/8/12 dan K.O. dari `Feel.ko.state`, bertepuk saat kombo >= 5 berakhir), kabut dan hujan dekat, kilat lembut. Vinyet dan kerudung warna depan adalah elemen DOM `#arena-vig` tepat di atas kanvas (nol biaya gambar). Biaya dijaga: tanpa vinyet berskala, blend besar memakai source-over, kabut berupa ubin siap-ukuran digambar 1:1, gradasi langit lewat strip 1x256; dengan rasterisasi perangkat lunak tiap operasi layar penuh terukur ms, jadi tambah efek besar dengan hati-hati. Pengaturan: baris "Efek latar" Penuh/Ringan/Mati disisipkan `arena.js` ke `#settings-panel` (`localStorage['konoha.arena.v1']`, bawaan Ringan bila `html.touch`); Mati = tidak ada yang digambar dan baris Cuaca di layar arena dimatikan. Layar arena: baris Cuaca (Otomatis/Cerah/Hujan/Kabut/Senja/Malam) disisipkan di sisi kiri tombol mulai (ruang yang tadinya kosong), jadi tinggi layar tidak berubah di 1280x720 dan 1024x520. Gerak dikurangi (`Access.motion` atau prefers-reduced-motion): waktu latar dibekukan, burung/perahu/daun/debu/bintang jatuh/kilat dan lompatan penonton dimatikan. Arena baru: tambah satu entri di `ARENA` (horizon, cloud, keep, crowd, dst; lihat komentar di atas tabel). Konsol: `Arena.state()`, `Arena.force('hujan')` (null melepas), `Arena.setFx('light')`; URL `?cuaca=malam` memaksa cuaca di semua pertandingan. Kegagalan gambar mematikan modul sendiri (loop game tidak terganggu). Belum ada skenario otomatis (mode ringan, diuji manual); `precache.js`/`files.json` perlu dibangun ulang oleh integrator agar `arena.js` dan `arena.css` ikut tersimpan offline.
|
||
- 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)`.
|
||
- Gamepad dan ubah tombol (Tahap 4.5): `input.js` + `input.css` (`window.Input`), dimuat tepat setelah `touch.js`; satu-satunya kait ke game.js adalah `__game.input` (plus pembungkus `__game.startMatch/setP2` untuk mengetahui mode dua pemain; bila kelak ada `__game.p2Active()`, itu yang dipakai). **Ubah tombol** (Pengaturan > "Ubah tombol…", dialog `#keymap-dialog`): 11 aksi Pemain 1 disimpan di `localStorage['konoha.keymap.v1']` sebagai `{aksi: {c: e.code, l: label}}`. Hanya bila pemetaan berbeda dari bawaan, keydown/keyup ditangkap di fase capture pada `window`, diterjemahkan ke kunci kanonik (a d w s spasi i o p k l j) lewat `__game.input`, dan event aslinya dihentikan; tombol bawaan yang sudah dipindah ditelan (termasuk keyup-nya, supaya game.js tidak melepas kunci kanonik yang ditahan lewat tombol baru). Tidak aktif di menu depan (`body.in-menu`), dialog terbuka, layar VS/kartu cerita, atau kolom teks; Spasi/Enter pada `<button>` dibiarkan seperti di game.js. Tidak bisa dipetakan: panah, N, M, koma, titik (Pemain 2), Esc, R, Enter, Tab, Ctrl/Alt/Cmd, tombol F; bentrok antaraksi = kedua tombol ditukar. Bantuan kontrol di Pengaturan ditulis ulang dari teks aslinya (5 baris pertama), label `kbd` di HUD (kartu skill, meter ultimate; meter ditulis ulang game.js tiap keadaannya berubah jadi diamati) dan Daftar gerakan menu jeda (hanya bagian Kontrol dasar, Serangan dan skill, Gerakan perintah, Mode) mengikuti pemetaan lewat MutationObserver tanpa mengubah game.js. Daftar gerakan di menu Karakter (meta.js) dan teks "tombol K/I/O" di toko belum mengikuti. **Gamepad**: polling `requestAnimationFrame` hanya selama ada gamepad (tata letak standar; pad pertama = P1, pad kedua = P2 bila mode dua pemain). Di arena: stik/D-pad = a d w s, A/Cross = w, X/Square = spasi, Y/Triangle, B/Circle, R1 = i o p, L1/L2/R2 = k l j, Start = Esc; pad P2 memakai kunci P2 (panah, n m , .). Di menu depan: keydown sintetis ArrowKeys/Enter/Escape yang ditangani menu.js (bila tidak ditangani, fokus dipindah dan tombol diklik); di dialog (pengaturan, jeda, ubah tombol), hamparan Meta, layar VS dan kartu cerita: fokus dipindah langsung, A = klik/Enter, B/Start = tutup (Esc lewat `__game.input('escape')`). Kontrol yang sedang ditekan saat konteks berganti diabaikan sampai dilepas. Getaran lewat `vibrationActuator` saat `hero.hp` turun (dimatikan lewat Pengaturan, `localStorage['konoha.pad.v1']`). Gamepad API hanya jalan di `localhost`/HTTPS. Belum ada skenario otomatis (diuji manual).
|
||
- Tata letak sentuh yang bisa diatur (RENCANA 4.6): `touchlayout.js` + `touchlayout.css`, dimuat `defer` di `<head>` index.html (satu baris tag baru bersama manifest dan `pwa.js`); hanya aktif di `html.touch`, `touch.js` tidak diubah. Pengaturan mendapat baris `#tl-row` (disisipkan lewat JS sebelum `.dialog-buttons`) yang membuka editor `#tl-editor`: `<dialog>` modal layar penuh yang menumpuk di atas Pengaturan (Pengaturan disembunyikan lewat `html.tl-editing`, tetap terbuka sehingga `settingsOpen` menahan game; Esc ditangkap fase capture agar game tidak menutup Pengaturan). Di dalamnya ada salinan (`cloneNode`, tanpa pendengar dan `inert`) `.dpad`, `.action-pad`, `.extra-pad` plus kotak seret `.tl-box` dengan pegangan sudut; panah/+/- juga bekerja di papan ketik. Hasil ke kontrol asli hanya kelas `.tl-xf` + variabel `--tl-x/--tl-y/--tl-s` (`transform: translate() scale()`, titik tumpu sudut dekat tepi); elemen bawaan tidak disentuh sama sekali. Simpanan `localStorage['konoha.touchlayout.v1']` = `{v:1, g:{dpad|action|extra:{x,y,s}}}` (x,y = pecahan lebar/tinggi layar, s = skala 0,6-1,8). Penjepitan ke layar memakai `getBoundingClientRect` nyata (area = layar - tepi aman - 4 px - bilah editor 46 px di atas), dijalankan `ResizeObserver` pada `#touch-controls` saat kontrol tampil/ukuran berubah; nilai simpanan tidak ditimpa penjepitan. `window.TouchLayout` = `{open(), editing, layout}` (konsol: `TouchLayout.open()`). Belum ada skenario otomatis dan belum pernah dijalankan di peramban oleh penulisnya (hanya `node --check` dan smoke desktop): uji manual di 844x390 dengan `?touch=1`.
|
||
- PWA (RENCANA 4.7): `manifest.webmanifest` (id `konoha-fighting`, `display: fullscreen` + `display_override [fullscreen, standalone]`, landscape, warna `#101923`, ikon PNG any dan maskable 192/512) ditautkan di index.html dan mobile.html bersama `rel=icon` dan `apple-touch-icon`. Ikon di `assets/pwa/` dibuat `tools/make_pwa_icons.py` (hanya Pillow) dari `assets/menu/browo-select.webp`; jalankan ulang bila ingin mengganti gambar. `pwa.js` (`window.KonohaPWA = {available(), install(), onChange(fn)}`) dimuat sinkron di `<head>` kedua halaman karena `beforeinstallprompt` datang ke dokumen paling atas: di HP itu `mobile.html`, dan iframe game memakai `window.parent.KonohaPWA` (panggilan langsung supaya gestur ketuk tetap berlaku untuk `prompt()`). Baris `#pwa-row` ("Pasang aplikasi") di Pengaturan hanya tampil bila event tersedia dan belum terpasang; iOS tidak punya event itu, jadi di sana tombol tidak tampil. `sw.js`: perilaku lama dipertahankan (gambar/font cache dulu, HTML/CSS/JS jaringan dulu); navigasi yang gagal dan tidak ada di cache mendapat `offline.html` dari cache kecil `konoha-offline-v1` (terpisah dari `aether-assets` karena `preload.js` menghapus entri yang tidak ada di `precache.js`). `tools/build_precache.py` kini memuat `manifest.webmanifest` dan `assets/pwa/*` (`offline.html`, `pwa.js`, `touchlayout.*` ikut otomatis); `precache.js`/`files.json` perlu dibangun ulang oleh integrator. mp3/mp4 tetap dilayani jaringan, jadi tanpa internet game jalan tanpa musik. Server perlu MIME `application/manifest+json` untuk `.webmanifest` (sudah di `.htaccess`).
|
||
- Bantuan dan aksesibilitas (Tahap 4.2 dan 4.3): `access.js` (`window.Access`) + `access.css`, dimuat tepat sesudah `feel.js`. Menyisipkan kotak lipat "Bantuan & aksesibilitas" ke `#settings-panel` (tombol biasa, bukan `<details>`, karena penangan tombol game mencegat Spasi pada `<summary>`), setiap pilihan punya tombol Atur ulang (muncul bila nilainya bukan bawaan) plus "Atur ulang semua". Simpanan `localStorage['konoha.access.v1']` hanya berisi nilai non-bawaan; `konoha.access.open` mengingat kotak terbuka. Id pilihan: `combo`, `block`, `nocd`, `regen` (true/false), `text` (0 Normal, 1 Besar, 2 Sangat besar), `cb` (`off`|`pro`|`deu`|`tri`), `calm` (`auto` mengikuti prefers-reduced-motion | `on`), `hc` (true/false). Penerapan lewat kelas pada `<html>`: `acc-t` + `acc-text-1|2` (variabel `--acc-z` teks kecil dan `--acc-zm` teks sedang, dipakai lewat `zoom` pada elemen teks saja), `acc-cb` + `acc-cb-pro|deu|tri` (filter SVG `#acc-cb-*` pada `#game-shell`, dibuat access.js; bar HP lawan bergaris dan lapisan cadangan bertitik), `acc-hc`, `acc-calm` (cermin aturan prefers-reduced-motion yang sudah ada di berkas CSS lain). Mesin membaca `window.Access` secara malas dan tanpa modul ini semua kait menjadi no-op. Kait di game.js, semuanya hanya sisi pemain (`hero`): `startAttack` (Kombo otomatis: pukulan 1 dibuat dengan `queued: 2`), `heroGuard` + `step` (Blok otomatis: serangan dari depan saat di tanah, tidak beraksi, guard belum pecah, masuk jalur blok biasa lalu `hero.autoGuard` menahan sikap blok 0,3 dtk; guard tetap bisa pecah), `step` (`Access.capCooldowns`: skill 1 dan 2 selalu 0, ultimate paling lama 5 dtk), `ultMax` dan pie cooldown (`Access.cdMax` agar bilah ikut batas 5 dtk), `useShopMove` (`shopCd` 0), `passiveRegen` (+2 HP/dtk, aturan 3 dtk tanpa terkena tetap berlaku), `draw`/`drawFlash`/`updateHud` (`Access.motion`: 0 = tanpa getaran layar, sentakan, zoom kamera Bro Wo, kilat sprite, potongan ultimate meluncur). `feel.js` memakai pengali yang sama lewat `motion()`/`calm()` (zoom K.O. dimatikan, kilat K.O. dimatikan, vignette tetap, animasi peringkat kombo berhenti) dan `vs.js` menghentikan hitung naik koin. Saklar "Animasi latar menu" dipadamkan lewat saklar aslinya (menu.js tidak diubah). Hadiah koin dan pencapaian tidak dibedakan saat bantuan aktif. Uji manual cepat di konsol: `Access.set('nocd', true)`, `Access.set('cb', 'deu')`, `Access.set('text', 2)`, `Access.state()`, `Access.reset()`. `precache.js`/`files.json` perlu dibangun ulang oleh integrator agar `access.js` dan `access.css` ikut tersimpan offline.
|
||
- Tutorial singkat (Tahap 4.1): `tutorial.js` (`window.Tutorial`) + `tutorial.css`, dimuat di `index.html` tepat sebelum `menu.js`. Tombol TUTORIAL kecil di pojok kiri atas layar utama (dipasang `Tutorial.decorate(root, screen)`, dipanggil di akhir `render()` menu.js; tidak menambah baris tombol, jadi menu tetap muat di 520 px) memulai `startMatch({mode:'training', level:'easy', stage:'bellora'})` dengan karakter terakhir yang dimainkan (atau Mega Chan) melawan Soloman (atau Mega Chan) sebagai dummy diam. Delapan langkah (`STEPS` di tutorial.js: gerak, lompat, kombo, gerakan perintah, skill, blok dan parry, lempar, ultimate) dideteksi per frame dari `__game.hero` (`state`, `jumps`, `action.type/special`, `blocking`, `cooldowns`, `spCd`, `throwCd`) dan dua kait baru di game.js: `__game.stats()` (`comboHits`, `bestCombo`, `totalDamage`) serta `__game.dummyAttack(slow)` (serangan dasar pertama dummy, hanya di latihan; durasi dikali `slow` supaya pembuka parry terbaca; `index` disetel 3 supaya `cpuFollowUp` tidak merangkai serangan kedua). Hasil serangan dummy (parry, blok, kena) dibaca dari pembungkus `__game.receiveHit` yang dipasang tutorial (hanya mengamati, tidak mengubah hasil). Selesai = +50 koin satu kali (`Meta.addCoins`); tandanya `Meta.save.eggs['code:tutorial']` (awalan `code:` supaya tidak dihitung sebagai kejutan oleh `mystery.js`). Panel `#tut-panel` ada di `#game-shell`, tengah atas di bawah header HUD (letaknya mengikuti tiga ukuran header di style.css); `#announcement` digeser ke bawah panel lewat `body.tut-on` dan `--tut-bottom`. Tutorial berhenti sendiri bila menu utama terbuka (`body.in-menu`). Di `html.touch` teks dan sasaran memakai tombol layar. Belum ada skenario uji otomatis; cek cepat di konsol: `Tutorial.state()`, `Tutorial.start({player:'browo'})`.
|
||
- `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).
|
||
- `vs.js` (`window.VS`, `window.ResultView`) + `vs.css` + `content/quotes.js` (`window.QUOTES`: intro/win/lose per karakter): layar VS dan layar hasil. `startMatch({..., vs: true | {label, p2}})` menampilkan layar VS 2,2 dtk (hanya mode versus; latihan tidak); selama itu `vsHold` di game.js menahan `advanceMatch` dan cue 'Round 1' keluar tepat sekali saat VS selesai atau dilewati (klik/Spasi/Enter/Esc setelah 0,38 dtk). `startMatch` tanpa `vs` berperilaku seperti dulu. `menu.js` screen `result` memakai `ResultView.view/mount` (layar lama jadi cadangan); `Meta.award` menyimpan rincian di `Meta.lastAward`; `Meta.nextFight`/`finishRun` memakai VS dan layar hasil yang sama. Pemilih kutipan `VS.quote(id, 'intro'|'win'|'lose')` memakai pengacak kosmetik terpisah. API uji: `__game.vsActive()`, `__game.vsSkip()`. Menu depan yang tersembunyi dijamin hilang oleh `#front-menu[hidden]{display:none!important}` (menu.css); aturan `display` per `data-screen` tidak boleh menimpanya. Layar hasil memfokuskan wadah `.rs` (`tabindex=-1`), bukan tombol, agar Spasi/Enter dari pertarungan tidak langsung memicu MAIN LAGI (Tab tetap menjangkau tombol). `Meta.toast` menahan toast selama `VS.active` dan memunculkannya sesudah VS selesai.
|
||
- Dialog pasangan dan easter egg berpasangan (Tahap 3.1 dan 3.4): `content/dialogs.js` (dimuat sebelum `vs.js`) mengisi `window.DIALOGS['<idA>|<idB>'] = {a, b}` untuk 21 pasangan (kunci urut alfabet; a = ucapan idA) dan `window.DIALOGS.rahasia` (empat pasangan tersembunyi, bentuk sama, bukan bagian dari 21 kunci). `VS.dialog(idPemain, idLawan)` -> `{key, secret, l, r, reply}` memetakan ucapan ke sisi kiri (pemain) dan kanan (lawan), atau `null` bila pasangan tidak punya dialog (karakter sama) sehingga kutipan umum `QUOTES` jadi cadangan. Pasangan ber-dialog menampilkan dua balon `.vs-quote.vs-bubble` (pembuka = ucapan `a` muncul dulu, pembalas = ucapan `b` menyusul lewat kelas `.re`, apa pun sisi pemainnya) dan layar VS berlangsung `VS.durationDialog` (3,3 dtk; `VS.duration` 2,2 dtk dipakai tanpa dialog); gaya di bagian "Dialog pasangan" `vs.css`. Dialog rahasia menggantikan dialog biasa setiap pasangan itu bertemu (balon keemasan `.secret`, label `.vs-secret` DIALOG RAHASIA) dan memanggil `Meta.pairEgg(kunci)`: +75 koin dan satu toast hanya sekali per pasangan (`Meta.save.eggs['dialog:<kunci>']`), pencapaian `reuni` (tampil ??? sampai terbuka, deskripsi umum tanpa menyebut pasangan) setelah semua kunci `DIALOGS.rahasia` ditemukan. Toast ikut tertahan selama VS (aturan `Meta.toast`). Berlaku di versus, 2 pemain, Arcade, Survival; latihan tidak punya layar VS. Pedoman isi sama dengan `quotes.js`; Belum ada skenario otomatis khusus dialog (diuji manual; cek cepat: `__game.startMatch({player:'soloman',enemy:'sonofsolo',stage:'harbor',level:'easy',mode:'versus',vs:true})`, lalu `Meta.save.eggs` dan `VS.dialog(idPemain, idLawan)`). Pengujian yang menunggu layar VS selesai harus menunggu `!__game.vsActive()` (bukan tidur 2,2 dtk) karena waktunya kini bergantung pada ada-tidaknya dialog.
|
||
- HUD (game.js): meter ultimate `#ult-player`/`#ult-enemy` (`updateUltMeters`; mengisi/SIAP/waktu mode dari `modeOf`, `bwx.rage`, bentuk FENR; DOM hanya ditulis saat tampilan berubah). Menu jeda `#pause-panel` dua panel; `buildMoveList()` membangun ulang Daftar gerakan tiap `setPaused(true)`, pilihan lipat diingat di `localStorage['konoha.moveList']`.
|
||
- Mode cerita Arcade dan bos Bayangan (Tahap 3.5 dan 3.6): `content/story.js` (`window.STORY`: `intro[id]` 2-3 kalimat, `between[A][B]` 1-2 kalimat (atau fungsi), `ending[id]` 3 kalimat, `boss` = pembuka, ucapan VS/kalah/menang, kalimat akhir, gelar; `generic` cadangan dengan penampung `{name}` `{rival}` `{n}`) dan `story.js` + `story.css` (`window.Story`). `Story.show({kind: intro|between|boss|ending, player, enemy, step, total, stage, boss}, onDone)` menampilkan kartu layar penuh (`#story-screen`, z-index 45, di atas layar VS 40) dan memanggil onDone tepat satu kali: klik/ketuk/Spasi/Enter/Esc setelah 0,5 dtk, atau otomatis (kartu antar-lawan sekitar 4 dtk, kartu panjang lebih lama); selama tampil semua tombol ditahan dan `Meta` menahan toast (`Story.active`). Mengembalikan false tanpa kartu bila tidak ada teks, jadi alur tetap jalan tanpa cerita. `meta.js` Arcade: `startRun` (kartu pembuka) -> `nextFight` -> menang -> `betweenFights` (kartu antar-lawan, panggung lawan berikutnya jadi latar kartu) ... lawan terakhir kalah -> `arcadeCleared` (hadiah tamat 500 + pencapaian langsung dibayar seperti dulu) -> kartu bos -> `startBoss` -> `finishRun(true, 'win'|'lose')` -> kartu akhir -> layar hasil. Survival tidak memakai cerita. `run.boss` = null | 'pending' | 'fight'. Menang atas Bayangan: +1000 koin, pencapaian BAYANGAN (+50), gelar PENAKLUK BAYANGAN (`Meta.save.title`, tampil di kartu akhir dan plat pemenang layar hasil); kalah: Arcade tetap tamat, tanpa bonus bos. Uji manual: `?arcade=N` memendekkan Arcade menjadi N lawan. Bayangan = `startMatch({player: X, enemy: X, level: 'excellent', shadow: true})` (id sama; state `hero` dan `dummy` tetap terpisah): `setShadowFoe` mengatur `BALANCE.dummyMax` (+40%), `foeDamage` menambah 10% pada damage ke pemain (pembungkus `__game.receiveHit` di akhir game.js dan `dotDamage`), `dummyHpShown` memberi skala HP dasar untuk bar dan penentu waktu habis (`MatchRules.tick`), `drawDummy` memakai `ctx.filter` gelap + lapisan ungu `drawShadowTint` + aura, percikan ungu lewat `fxr()` di `updateVisuals`. HUD: nama BAYANGAN dengan label nama asli, kelas `.shadow-foe` (story.css). VS: game.js memanggil `Story.decorateVs(info)` tepat setelah `VS.show` (nama 'BAYANGAN <NAMA>', ucapan bos); layar hasil: `ResultView.view` membaca `run.boss` (Bayangan di sisi kalah; kalah dari Bayangan tetap tampil TAMAT). `__game.shadowFoe()` melaporkan keadaan.
|
||
- Statistik pertandingan (RENCANA 5.5): `stats.js` (`window.MatchStats`) + `stats.css`; tag-nya ada di baris sendiri sebelum `</main>` di `index.html`. Statistik pemain 1 per pertandingan versus (juga 2 pemain, Arcade, Survival, Bayangan; latihan = `reset(false)`, tanpa statistik). Kait di game.js, semuanya satu baris dengan `window.MatchStats?.`: `reset(versus)` di `startMatch` dan `reset`; `tick(dt, match, hero.hp, dummy.damage, BALANCE.dummyMax)` di awal `advanceMatch` (damage keluar/diterima = selisih HP per langkah, durasi = jumlah waktu fase fight, ronde = peralihan fight -> ko); `cast(jenis, hero.action)` di `cast` dan `startAttack`; `hit(hitSource(), counter, comboHits, hero.action)` di `hitDummy` (juga jalur chip blok CPU) dan `hit(null, ...)` di burst `dotDamage`; `parry()`/`block()` di `heroGuard`; `finish(info)` di event 'complete' menaruh ringkasan di `info.stats` (dibawa `Meta.award` dan `FrontEnd.showResult`). Akurasi jurus = pemakaian yang kena sekali; pukulan proyektil/ultimate dikaitkan ke jurus terakhir dalam jendela waktu (`MatchStats.WINDOW`), jadi bila dua jurus bertumpuk bisa meleset satu. Layar hasil: `ResultView.view` memanggil `MatchStats.resultHtml` (tombol STATISTIK di kanan baris skor + kartu `.rs-ms` yang menutup kolom tengah di atas tombol aksi, diatur `MatchStats.mount`; Esc menutup kartu lebih dulu daripada kembali ke menu); tanpa `info.stats` (data uji) tidak ada tombol. Simpanan seumur hidup: `Meta.award` memanggil `MatchStats.commit(save.stats, r.stats)` (kunci baru `dmgDealt, dmgTaken, parries, blocks, throws, counters, rounds, playSec, statMatches`; `bestCombo` ikut dinaikkan ke kombo terbaik seluruh ronde; simpanan lama tanpa kunci terbaca 0), tampil di Koleksi lewat `MatchStats.lifetimeHtml`. Uji manual cepat: `MatchStats.snapshot()` di konsol saat pertandingan berjalan. Belum ada skenario otomatis khusus statistik (mode ringan, diuji manual).
|
||
- `assets/<id>/` atlas sprite (15 state), `manifest.js` (data + metrik), `ui/`; `work/` (di repo induk, luar folder ini) berisi pipeline pembuat karakter.
|
||
- `precache.js`/`files.json`: **dibuat otomatis** oleh `tools/build_precache.py` — jangan diedit atau di-commit oleh cabang fitur; perintahkan integrator membangun ulang.
|
||
|
||
## API uji: `window.__game`
|
||
`startMatch({player,enemy,stage,level,mode})` (mode `versus`|`training`), `advance(detik)` (memajukan simulasi tanpa menunggu waktu nyata), `press(key)`/`release(key)` (a d w s spasi i o p k l j; P2: arrowleft arrowright arrowup n m , .), `setAI(bool)`, `setP2(bool)`, `snapshot()`, `hero`, `dummy`, `projectiles`, `effects`, `draw()`, `receiveHit(dmg)`, `command(id)`. Layar tidak diperbarui saat `advance`; panggil `draw()` sebelum screenshot.
|
||
|
||
`__game.hitDummy`, `__game.feel` dan `__game.feelState()` membuka tabel rasa kena, status K.O./angka untuk `tests/t1-feel.mjs`. `__game.fx.snapshot()` melaporkan keadaan efek (bayangan, genangan api, DoT, aba-aba, proyektil beserta gaya/umur) untuk skenario uji. Untuk uji deterministik set `__game.manual = true` dulu; kalau tidak, loop waktu nyata ikut memajukan simulasi saat screenshot.
|
||
|
||
## Alat uji headless
|
||
`node tests/harness.mjs smoke` — 7 karakter × easy/hard melawan CPU, gagal bila ada galat halaman. `node tests/harness.mjs tests/foo.mjs` — menjalankan skenario: `export default async ({ page, game, errors, characters }) => hasil` (`game(fn, arg)` = `page.evaluate`). Screenshot: `await page.screenshot({path})` lalu `Read` gambar. `tests/t1-fx.mjs` melepas semua jurus enam karakter berbasis data dari kedua sisi (env `FX_SHOTS` = folder screenshot, `FX_ONLY` = regex nama jurus). Node dan Playwright sudah terpasang; tidak perlu `npm install`. `tests/t2-hud.mjs` (env `HUD_ONLY=meter|enemy|cpu|pause|layout`) menguji meter ultimate dan menu jeda; `tests/t2-vsresult.mjs` (env `VSR_SHOTS`, `VSR_ONLY=quotes|vs|skip|abort|result|run|ui|focus|phone`) menguji layar VS, layar hasil, dan kutipan. Skenario `tests/t2-vsresult.mjs` menunggu `!__game.vsActive()` (bukan tidur 2,2 dtk) karena layar VS ber-dialog 3,3 dtk. `tests/t2-brand.mjs` (env `BRAND_SHOTS`, `BRAND_ONLY=text|quote|log|menu|flow|docs`) menguji sisa nama game asal di seluruh layar, kutipan loading, Yang baru dan toast, serta latar kolase. **Satu perintah untuk semuanya: `node tests/all.mjs`** (sintaks semua berkas JS, smoke, 49 pasangan arah singkat, data peringkat dan Koleksi; lihat bagian Tahap 5 di bawah).
|
||
|
||
## Simulator keseimbangan, peringkat, dan uji otomatis (Tahap 5)
|
||
- `tests/bot.mjs`: `installBalance()` dijalankan di dalam halaman (`page.evaluate(installBalance)`) dan memasang `window.BalSim = { bot, play, round, think, reset, ... }`. Game tidak diubah: bot hanya memakai API uji (`__game.press/release/advance/startMatch/snapshot/hero/dummy/projectiles/config/balance`) dan `__game.manual = true`. Bot menggerakkan sisi hero; sisi lawan dikendalikan CPU bawaan. Bot mendekat (berlari lewat ketuk dua kali bila jauh), menyambung kombo hanya bila pukulan berikutnya akan mengenai (`landsAt`: lawan tidak kebal, jatuh, atau di luar stun), memakai skill 2 lalu skill 1 saat jaraknya cocok (jangkauan dibaca dari data karakter, jadi ikut berubah saat angka disetel; Bro Wo ditulis tangan), mengakhiri kombo dengan skill, menjaga jarak saat lawan kebal, memblok atau parry sebagian serangan yang terbaca setelah jeda reaksi ~0,17 dtk, melompati proyektil rendah, dan **membalas ultimate lawan dengan ultimate** (atau memakainya sendiri setelah 8 dtk; di mode pasca-ultimate setelah 2 dtk) sambil menunggu lawan bangun. Tidak memakai lempar, jurus perintah, atau jurus toko, dan tidak menghindari genangan api/asam. Penyetelan bot ada di objek `C` (bisa ditimpa lewat env `BAL_BOT`).
|
||
- `tests/balance.mjs` (simulator): `node tests/harness.mjs tests/balance.mjs [tanggal]`. Tiap pasangan 7 x 7 (termasuk cermin) dimainkan `BAL_N` ronde per arah (bawaan 6), di kedua posisi, tanpa render (`advance(dt, true)`). Satu ronde = `startMatch` (gaya Seimbang, arena Bellora, tingkat `BAL_LEVEL`, bawaan hard) lalu bot bermain sampai fase K.O.; simulator berhenti tepat di fase itu, jadi `Meta.award` dan layar hasil tidak pernah terpanggil. Hasil per ronde: pemenang, HP akhir, durasi, waktu ultimate pertama tiap sisi, bendera jurus terpakai. Env: `BAL_N`, `BAL_LEVEL`, `BAL_DATE` (atau argumen ke-2; tanggal diberikan, bukan `Date`), `BAL_SEED`, `BAL_ONLY=a,b`, `BAL_WORKERS=k` (k proses Chromium paralel), `BAL_BOT` (json), `BAL_REPORT` (bawaan `docs/balance-report.md`, `none` = jangan tulis), `BAL_JSON` (simpan hasil ringkas), `BAL_BASE` (hasil "sebelum" untuk tabel perbandingan), `BAL_TIERS=1` (tulis `content/tiers.js`), `BAL_LABEL`. Deterministik untuk benih, N, dan jumlah pekerja yang sama (pengacak mesin tidak diatur ulang tiap ronde, jadi urutan ronde ikut menentukan). Satu ronde memakan ~0,1 dtk nyata; N=8 (392 ronde) sekitar 30 sampai 50 dtk dengan 2 pekerja. Bagian laporan di bawah penanda `catatan-manual` dipertahankan saat laporan dibuat ulang. Cara membaca hasilnya dan batasannya ada di `docs/balance-report.md` (bot menang ~80% karena keunggulan posisi hero; persen menang per karakter menggabungkan kedua posisi).
|
||
- Peringkat (RENCANA 5.2): `content/tiers.js` (`window.TIERS = {updated, order, rate, tier}`) **dibuat otomatis** oleh simulator (`BAL_TIERS=1`), jangan diedit tangan; batas tier di `TIER_CUT` pada `tests/balance.mjs` (S >= 58, A >= 52, B >= 46, selebihnya C). `meta.js` (`tiersHtml`, `loadTiers`) memuat berkas itu secara malas lewat elemen script saat Koleksi pertama dibuka (index.html tidak diubah) dan menampilkan "Peringkat karakter (versi simulator)" dengan keterangan bahwa angkanya bukan kebenaran mutlak; tanpa berkas atau bila gagal dimuat, bagian itu tidak tampil. Berkas baru ini ikut `tools/build_precache.py` (integrator membangun ulang precache).
|
||
- Penyetelan angka (RENCANA 5.3): riwayat sebelum dan sesudah, daftar angka yang diubah, dan temuan (rangkaian ultimate menentukan hampir semua ronde, CPU hampir tidak pernah memakai skill 2 area tanpa `ahead` milik Ethanol Man dan Solo Man) ada di `docs/balance-report.md`. Setelah mengubah angka jurus atau pasif, jalankan ulang simulator dengan `BAL_BASE=docs/balance-after.json` supaya laporan membandingkan dengan keadaan sebelumnya.
|
||
- Uji otomatis satu perintah (RENCANA 5.4): `node tests/all.mjs` menjalankan (1) `node --check` untuk semua berkas JS proyek, (2) `node tests/harness.mjs smoke`, (3) `node tests/harness.mjs tests/pairs.mjs`: 49 pasangan arah dimainkan singkat oleh bot (env `PAIRS_SECONDS`, bawaan 24 dtk permainan), memeriksa galat halaman/konsol, angka tidak wajar (NaN, HP di luar 0..200), bahwa tiap karakter memakai skill 1, skill 2, dan ultimate di kedua sisi (sedikitnya satu dari 7 ronde per sisi; celah CPU yang sudah diketahui, `KNOWN_CPU_GAPS`, hanya diperingatkan), data `TIERS`, dan bagian peringkat di Koleksi. Keluar dengan kode 0 bila lolos dan 1 bila ada yang gagal; `ALL_SKIP=sintaks,smoke,pasangan` melewati langkah. Total sekitar 1 menit.
|
||
|
||
## Aturan kerja
|
||
- Jangan memakai `Math.random`/`Date.now` untuk logika yang memengaruhi hasil pertandingan bila ada pengacak `random()` bawaan (benih tetap).
|
||
- Tampilan harus tetap muat di jendela setinggi 520 px; jangan memotong menu.
|
||
- Tidak ada pustaka eksternal baru. Tidak ada gambar baru (tidak boleh memanggil generator gambar).
|
||
- Commit atas nama `rizkyaz0 <rizkyadizaelani@gmail.com>`, tanpa baris Co-Authored-By atau atribusi apa pun. **Jangan pernah `git push`** dan jangan mengubah remote.
|
||
- Teks HUD di `index.html` (nama, kelas, jurus) hanya placeholder netral; `game.js` menimpanya lewat `fighterInfo`. Cabang lama di `fighterInfo` dan manifest `assets/mecha`, `assets/fenr` adalah sisa game asal yang tidak terjangkau dari roster; jangan menampilkannya ke pemain.
|
||
- Jaga gaya kode yang ada (ringkas, satu baris per pernyataan kecil, komentar hanya bila perlu).
|