fighting-konoha/docs/ENGINE_NOTES.md

44 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).
- `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`.
- `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`.
- 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.
- `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.
## 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).