# 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//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`. - 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)`. - 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 `
`, karena penangan tombol game mencegat Spasi pada ``), 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 ``: `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. - `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/-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['|'] = {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:']`), 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 ', ucapan bos); layar hasil: `ResultView.view` membaca `run.boss` (Bayangan di sisi kalah; kalah dari Bayangan tetap tampil TAMAT). `__game.shadowFoe()` melaporkan keadaan. - `assets//` 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. ## 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 `, 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).