fighting-konoha/docs/ENGINE_NOTES.md

57 KiB
Raw Blame History

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).
    • CPU, jurus area pemusat-diri dan rival adaptif (RENCANA 6.5/6.6): cpuAreaKit() (jurus area siap yang jangkauannya tidak melebihi pukulan dasar: hanya TAWA MELEDAK Ethanol Man dan SARUNG PUTAR Solo Man; A Nice, ahead 110, tidak termasuk) dan cpuAreaFits(P, m, dist) dipakai cpuPick, yang kini mendahulukan jurus itu bila lawan di dalam lingkarannya (selisih 12 px, sejajar tinggi, tidak sedang berjalan menjauh, pukulan pemain yang sedang disiapkan tidak mendarat lebih dulu, pemain yang berjalan masuk dihitung seperti walkIn); di blok Movement updateFenrAI, bila jurus itu siap CPU berdiri sekitar reach - 30 px (lebih rapat dari jarak pukulan dasar). Penyebab lama: pukulan dasar (161 px) selalu didahulukan di cpuPick dan menutup lingkaran 135/140 px, sedangkan penutup kombo cpuFollowUp tidak pernah lolos cpuLands (sisa stun lebih pendek dari 0,3 dtk waktu kena). Karakter lain tidak berubah (urutan random() mesin juga tetap). Batasan yang diketahui: CPU memukul dari jarak maksimum, jadi pemain yang menjaga jarak (seperti bot simulator, yang menjauh tepat saat CPU kebal) jarang masuk lingkaran; angka pemakaian skill 2 CPU di tests/pairs.mjs naik tetapi belum tinggi. ai.js (window.AIAdapt, lihat bagian "Latihan lanjutan dan rival adaptif" di bawah) mengubah salinan profil CPU di awal updateFenrAI.
    • 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).
  • Rekam ulang K.O. dan tangkapan layar (RENCANA 7.3 dan 7.4): record.js (window.Record) + record.css; tag di baris sendiri sebelum </main> di index.html, game.js tidak diubah. Kait lain hanya di vs.js: ResultView.view menambahkan window.Record?.resultHtml(r) di dalam .rs-main (toolbar .rs-rec yang melayang di pojok kanan atas kolom tengah, jadi tinggi layar hasil tidak bertambah) dan ResultView.mount memanggil Record.mount(root). Keadaan permainan dibaca dari DOM tiap 150 ms (poll): body.in-menu, #arena[data-mode|data-match-phase|data-character|data-opponent] (diisi updateHud tiap frame), dialog[open], VS.active/Story.active, document.hidden; __game.startMatch dibungkus hanya untuk memulai sesi baru seketika. Perekam: kanvas salinan <= 854 px lebar (640 di layar sentuh) diisi dari #arena ~30 kali/dtk oleh rAF sendiri (letterbox bila rasio jendela berubah), captureStream() -> MediaRecorder webm (vp8; vp9/webm cadangan), tanpa audio, 1,5 Mbps (0,9 di layar sentuh), timeslice 1 dtk. Potongan WebM tidak bisa dipangkas di depan (hanya potongan pertama memuat header dan keyframe), jadi dipakai dua perekam bergantian: perekam baru dimulai tiap 6 dtk waktu rekam, tiap perekam berhenti dan dibuang sesudah 12 dtk, klip = perekam tertua (6 sampai 12 dtk, satu berkas WebM utuh tanpa menyambung kontainer). Jeda/Pengaturan/layar VS/kartu cerita/tab tersembunyi = pause() (jam rekam ikut berhenti); menu atau pertandingan dibatalkan = dibuang; latihan tidak direkam. Saat matchPhase menjadi complete (atau resultHtml lebih dulu, karena layar hasil dipasang pada tick yang sama dengan event itu) perekam tertua dihentikan jadi klip dan bingkai terakhir arena dibekukan untuk tombol FOTO di layar hasil; klip dan bingkai dibuang saat pertandingan berikutnya mulai. Layar hasil: SIMPAN KLIP (konoha-fighting-<karakter>-vs-<lawan>.webm), TONTON ULANG (<dialog id=rec-replay> modal, Esc dan tombol B gamepad menutup), FOTO, BAGIKAN (navigator.share dengan berkas, selain itu unduh; PNG dibuat sinkron lewat toDataURL agar masih dalam gestur klik). HUD: tombol kamera di .match-actions (disembunyikan di lebar <= 620 px) dan penanda REC di bawah barisnya; menu jeda: baris TANGKAP LAYAR dan BAGIKAN; pintasan F9 (arena, jeda, layar hasil). Pengaturan: dua baris disisipkan setelah slider Suara (Rekam ulang K.O. = saklar utama; Indikator REC = penanda saja). Simpanan localStorage['konoha.record.v1'] = {on, badge}. Bila MediaRecorder/webm/captureStream tidak ada atau terjadi galat, klip dan baris Pengaturan hilang tanpa suara (tangkapan layar tetap jalan). Konsol: Record.state(), Record.set(false). Belum ada skenario otomatis (mode ringan, diuji manual); nilai di bagian atas record.js (PERIOD, FPS, bitrate di start) bisa disetel.
  • 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'}).
  • Latihan lanjutan dan rival adaptif (RENCANA 6.5 dan 6.6): training.js (window.Training) + training.css untuk panel dummy di arena latihan, ai.js (window.AIAdapt) untuk ingatan rival; tag-nya satu baris sendiri sebelum </main> di index.html (sesudah blok stats.js). Panel Dummy: tombol DUMMY dipasang ke bilah .match-actions (kelas .training-only, jadi hilang di versus dan menu; body.tut-on menyembunyikannya selama Tutorial dan semua kaitnya mati bila Tutorial.active). Isi: perilaku dummy (Diam bawaan, Blok terus, Lompat terus, Serang acak, Ikuti pemain, CPU penuh = saklar #ai-toggle/"Lawan aktif" yang sudah ada, tetap sinkron dua arah) dan Reset posisi; saklar Tampilkan hitbox (__game.setHitboxes, sinkron dengan #hitbox-toggle), Tampilkan frame data (startup/aktif/recovery/total dalam ms dari duration x hitAt seperti step(), jangkauan px, damage dasar, dibaca dari aksi pemain terakhir lewat __game.kitMove), Darah dummy tak terbatas (tidak pernah K.O.; terisi lagi 1 dtk sesudah kena terakhir); penghitung kombo/damage (__game.stats(), kini memuat comboDamage). Rekam/Putar: Rekam mereset arena (__game.reset()), menunggu tombol pertama, lalu menyimpan tombol yang diterima press() (plus release, forceCommand sentuh sebagai cmd:<id>, dan clearInput = lepas semua) dengan waktu simulasi relatif, maksimal 10 dtk; Putar mereset arena yang sama, menyetel benih random() mesin (__game.rng) seperti saat rekam dimulai, dan menjalankan peristiwa dari kait pre di AWAL tiap step() (sebelum waktu maju, jadi efeknya sama dengan tombol yang datang di antara dua langkah). Hasil putar dibandingkan dengan hasil rekaman (damage dan kombo terbaik): SAMA atau BERBEDA. Tidak terekam: klik kartu skill di HUD. Kait di game.js, semuanya window.Training?. satu baris: init (startMatch), pre (awal step), drive (cabang dummy tanpa AI di updateFenrAI; hanya match.mode === 'training'), blocks (kondisi blok di hitDummy; dummy berpose crouch, pukulan yang menjatuhkan dan lemparan tetap tembus), rec (press, release, forceCommand, clearInput), reset (resetCombat). Akses ke mesin: __game.dummyCtl (act, jump, move, reach, rand), __game.kitMove, __game.rng. Konsol: Training.state(). Rival adaptif: startMatch memanggil AIAdapt.reset(versus, level); updateFenrAI memanggil AIAdapt.apply(cpuProfile(), dt, aktif, hero, dummy) (aktif = CPU di versus, bukan P2 dan bukan latihan) yang mengamati pemain lewat keadaan arena (mulai melompat, mulai memblok, pukulan dasar index 1 atau gerakan perintah, skill dekat, skill proyektil/jauh >= 330 px) dalam jendela bergulir 45 dtk (paling banyak 36 kejadian, terbaca CPU 0,3 dtk sesudah terjadi) dan mengembalikan profil yang dikali pengali kecil dua arah: antiAir (pemain sering melompat), evade dan speed (sering menembak dari jauh), evade dan punish (sering memukul dekat), penambah aggression dan peluang skillBias yang diundi sekali per keputusan di cpuPick(..., skillFirst) (sering memblok). Kekuatan = tingkat (easy 0,1 / medium 0,5 / hard 0,85 / excellent 1) x keyakinan (butuh >= 8 kejadian, penuh di 20); porsi acuan "pemain biasa" di REF bukan data nyata. Tanpa pola semua pengali = 1 dan skillBias = 0, jadi perilaku dan urutan random() sama dengan sebelumnya. Layar hasil: ai.js membungkus ResultView.mount dan menyisipkan satu kalimat .rs-learn ("Rival belajar: kamu sering ...") bila ada kebiasaan dominan sepanjang pertandingan (>= 14 kejadian, kecenderungan >= 0,6), disembunyikan di jendela lebih pendek dari 600 px. Matikan lewat konsol: AIAdapt.enabled = false; intip: AIAdapt.state(). Berkas baru ikut tools/build_precache.py (integrator membangun ulang precache.js/files.json). Belum ada skenario otomatis (diuji manual).
  • 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 ', 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).
  • Kode tantangan dan kebiasaan bermain (RENCANA 7.5 dan 7.6): challenge.js (window.Challenge, window.Habits) + challenge.css; tag-nya sebelum menu.js di index.html. Kode = 8 karakter dari 30 simbol tanpa 0 1 I L O U (contoh TNNB-TPZM), isinya 24 bit (versi, variasi, aturan, tingkat, arena, lawan, pemain) + 15 bit pemeriksa yang dicampur; Challenge.encode({player, enemy, stage, level, rule}) / Challenge.decode(teks) (menerima huruf kecil, spasi, tanda hubung, tautan ?kode=, atau kalimat yang memuat kode; hasil {ok, cfg} atau {ok:false, err, msg}). Daftar karakter, arena, tingkat, dan aturan di berkas itu menentukan arti kode: urutannya tidak boleh diubah, hanya ditambah di belakang (karakter/arena baru yang belum didaftarkan ikut otomatis di belakang menurut urutan deklarasi). Aturan khusus (RULES, satu per kode): blitz Adu cepat (satu ronde, HP awal separuh), one Satu ronde, fast Lawan gesit (jalan +25%, cooldown pulih +35%), noblock Tanpa blok (blok dan parry mati untuk kedua sisi), heavy Pukulan berat (damage +50% kedua sisi); kuncinya hanya hidup di kode, tidak disimpan di localStorage (jadi tidak bentrok dengan aturan harian). Alur: tombol KODE TANTANGAN di pojok kiri atas menu utama (di sebelah TUTORIAL, absolut supaya tinggi menu tidak berubah) membuka dialog #ch-dialog yang memvalidasi sambil mengetik dan langsung memulai (Challenge.start(cfg) memanggil startMatch({... mode:'versus', vs:{label}, codeRule:{rule, code}})); ?kode=XXXXXXXX di alamat membuka dialog yang sama sebagai undangan setelah boot (parameter dihapus dari alamat sesudahnya; mobile.html meneruskan query ke iframe). Layar arena (hanya versus melawan CPU) mendapat baris ATURAN (menggilir aturan, berlaku juga untuk LET'S FIGHT dan MAIN LAGI lewat Challenge.pending(mode, p2) di menu.js start(); menu utama mengosongkannya) / SALIN KODE / TAUTAN di kolom kanan .match-rules; layar hasil mendapat SALIN KODE / TAUTAN di pojok kanan atas .rs-head (tidak untuk Arcade, Survival, dua pemain, atau Bayangan). Penyalinan: navigator.clipboard, lalu execCommand('copy'), lalu prompt(). Kait (semua window.Challenge?.x?.(), dibungkus guard: galat hanya dicatat di konsol, tanpa berkas ini game berjalan seperti dulu): game.js begin(options) di startMatch sesudah validasi dan sebelum refreshPassives (menetapkan aturan aktif, judul layar VS "TANTANGAN · ...", penanda .ch-badge di bawah jam HUD, dan mencatat kebiasaan), tune(sideMods) di akhir refreshPassives (aturan = pengali kunci passiveMod), roundStart(hero, dummy, BALANCE) di akhir resetCombat (HP awal), roundEnd(match, event) dan finish(info) di advanceMatch (satu ronde menetapkan match.winner sebelum efek K.O. akhir; finish mengingat pengaturan untuk SALIN KODE dan mematikan info.perfect pada aturan satu ronde supaya bonus/pencapaian BERSIH tidak murah), hud(match) di updateMatchHud (teks "SATU RONDE"), noBlock di press (tombol L) dan heroGuard; menu.js decorate(root, screen, {player, enemy, stage, level, mode, p2, runKind, result}) di akhir render(). startMatch mengabaikan codeRule untuk latihan, dua pemain, dan Bayangan. Aturan tidak memakai pengacak, jadi hasil pertandingan tetap deterministik. Kebiasaan bermain (Habits): localStorage['konoha.habits.v1'] = {v:1, n, ch, mode, stage, foe} (hitungan per karakter yang dipilih, mode, arena, dan karakter lawan; tanpa waktu dan identitas), dicatat di begin() tiap pertandingan dimulai (latihan juga; Bayangan tidak dihitung sebagai lawan; mode dikenali dari options.mode, vs.p2, awalan label ARCADE/SURVIVAL, codeRule, atau options.kind untuk mode baru). Terpisah dari Meta.save, jadi simpanan lama tak tersentuh dan data rusak dibaca sebagai nol. Bagian "Kebiasaan bermainmu" (karakter, mode, arena favorit, lawan paling sering, tombol HAPUS STATISTIK) disisipkan ke tab Koleksi oleh MutationObserver pada #meta-overlay (sebelum daftar pencapaian; meta.js tidak diubah); catatan privasi satu baris di layar About lewat decorate. Belum ada skenario otomatis dan UI belum diperiksa di peramban oleh penulisnya (hanya node --check, smoke, dan pemeriksaan penyandi sekali pakai: 5.880 kombinasi bolak-balik tanpa tabrakan): uji manual di 1024x520 dan 844x390. precache.js/files.json perlu dibangun ulang agar challenge.js/challenge.css ikut tersimpan offline. Konsol: Challenge.state(), Challenge.open(), Habits.data().
  • Tantangan harian dan hadiah harian (RENCANA 7.1 dan 7.2): daily.js (window.Daily) + daily.css, tag <script> paling akhir di index.html (sesudah meta.js, karena membungkus Meta.award) dan <link> CSS di <head>. Tanggal lokal -> hash -> satu tantangan yang sama untuk semua pemain: aturan (10 entri RULES: Darah satu, Hanya ultimate, Tanpa blok, Lawan cepat, Lantai licin, Tanpa lompat, Kombo wajib, Satu ronde, Counter saja, Raksasa), lawan, arena, dan tingkat (tiap aturan punya daftar levels; aturan yang menyulitkan hanya memakai easy/medium). Tombol kecil HARIAN dipasang MutationObserver pada #front-menu di layar home (item flex di .home-top-right, tepat di kiri teks versi, dengan margin atas dan bawah negatif setinggi setengah tombol sehingga kotak margin-nya 0 dan tinggi menu tidak berubah; sengaja bukan position:absolute dan .home-top-right tidak dijadikan induk posisi karena tombol layar penuh memakai .home-top sebagai induknya; tanpa kait di menu.js) dan membuka kartu <dialog id="daily-dialog"> (Esc dan panah ditangani pendengar keydown fase tangkap di window karena game.js menelan Esc): aturan, deskripsi, lawan, arena, tingkat, hadiah, streak, pilihan petarung (lawan hari ini tidak bisa dipilih). Mulai = __game.startMatch({..., mode:'versus', daily:'YYYY-MM-DD', vs:{label}}); pertandingan versus biasa. Daily.arm(options.daily, {sideMods, balance, versus}) dipanggil di SETIAP startMatch (setelah setShadowFoe), jadi pertandingan biasa otomatis mematikan aturan dan mengembalikan BALANCE.heroMax. MAIN LAGI di layar hasil mengulang tantangan yang sama (klik pada [data-cmd="rematch"] ditangkap fase capture dan menandai again). Menang = 150 koin + 25 per hari beruntun (maks +150) lewat Meta.addCoins, sekali per tanggal; pembungkus Meta.award menambah satu baris ke Meta.lastAward.lines (muncul di rincian koin layar hasil) dan menaikkan gain. Simpanan Meta.save.daily = {last, streak, best, total, greet, fest, pick} (ikut kode simpanan dan HAPUS SEMUA; simpanan lama tanpa kunci terbaca kosong; menyimpan = Meta.addCoins). 7.2: hadiah hari pertama +25 koin sekali per tanggal saat menu utama pertama tampil (toast), kejutan tanggal sekali per tahun (FEST: 1 Jan, 14 Mar Hari Pi, 8 Agu Hari Kucing, 17 Agu Dirgahayu 100 koin). Kait di game.js, semuanya window.Daily?.xxx dan no-op tanpa tantangan aktif: arm (startMatch), deny(jenis) (jump, startAttack, cast, startCommand, startThrow, useShopMove, press('l')), cooldowns dan slip (step), slip (updateDummy), hit (hitDummy, sesudah bonus counter), parry (heroGuard), foeScale (drawDummy). Aturan yang cukup dengan angka memakai sideMods dan BALANCE yang diberikan ke arm (HP Darah satu dan Raksasa, walk/cd lawan, parry lawan 0). HP pemain kini lewat heroHpShown() (skala 200 seperti dummyHpShown) untuk bar HP dan penentu waktu habis. Satu ronde: MatchRules.create dan tick dibungkus di daily.js (match.toWin = 1, pemenang ronde pertama ditetapkan sebelum game.js membaca match.winner); updateMatchHud membaca match.toWin. Layar VS: pembungkus VS.show menambah baris .vs-daily (label VS jadi TANTANGAN HARIAN); arena: #daily-tag di tepi atas tengah (berkedip saat aksi yang dilarang ditekan) dan body[data-daily-rule]. Tanggal tidak dipakai di dalam simulasi (hanya memilih aturan dan hadiah). Uji manual: ?daily=YYYY-MM-DD mengganti tanggal untuk seluruh modul (aturan, hadiah, streak); konsol Daily.setDate('2026-08-17') / Daily.setDate(null), Daily.table(14), Daily.status(), Daily.startup(), Daily.resetState(), Daily.open(), Daily.start('megachan'). Belum ada skenario otomatis; angka aturan (COUNTER_*, LICIN, LAWAN_*, HP_*) belum disetel dengan permainan nyata. precache.js/files.json perlu dibangun ulang oleh integrator agar daily.js dan daily.css ikut tersimpan offline.
  • assets/<id>/ atlas sprite (15 state), manifest.js (data + metrik), ui/; work/ (di repo induk, luar folder ini) berisi pipeline pembuat karakter.
  • Bahaya arena (RENCANA 6.2): hazard.js (window.Hazard) + hazard.css, dimuat tepat sesudah game.js. Satu kejutan per arena (tabel DEFS; arena tanpa entri tidak punya bahaya): bellora = genting jatuh (6 damage), sunspire = silau matahari (layar memutih, tanpa damage; CPU ragu 0,55 dtk), harbor = ombak dari kiri yang mendorong ke kanan sampai batas bertanda (3 damage, lompat atau berdiri di kanan garis batas untuk menghindar), elderwood = dua akar menyembul (5 damage), moonrise = petir arkana (7 damage). Tiap bahaya diberi aba-aba 1 detik (cincin/garis di lantai, kilau di langit, suara sound(), dan teks "Awas: ..." di baris kecil .hz-tag dalam .stage-sign; saat tenang barisnya "Bahaya: "). Aturan: hanya match.mode === 'versus' fase fight (latihan dan tutorial tidak); bahaya pertama 6-12 dtk sesudah ronde mulai, berikutnya 9-14 dtk sesudah aba-aba sebelumnya; tidak mulai di 4 dtk pertama, saat ultimate/sinematik (hazardCtx.busy), saat salah satu petarung jatuh, atau di 3,5 dtk terakhir ronde. Damage dikali pasif dmgIn sisi yang kena, tidak pernah mematikan (menyisakan 1 HP), tanpa luka/knockback, tidak menambah kombo, dan melewati petarung yang sedang kebal (invuln); zona = jarak datar ke pusat tanda (sama dengan cincin yang tergambar) dan tinggi kaki di atas lantai (70, petir 100), jadi lompat atau geser menghindar. Pemain, CPU, dan P2 kena aturan yang sama; CPU mencoba keluar dari zona lewat rencana sidestep bawaan (peluang = evade tingkat, jeda = reaction; hanya bahaya berzona, bukan ombak). Deterministik: pengacak LCG sendiri berbenih (benih mesin saat ronde mulai + nomor ronde + arena) dan waktu = dt simulasi; pengacak kosmetik terpisah, dan "Kurangi gerakan" (Access.motion) hanya mengubah efek (tanpa getar dan kilat putih, silau jadi semburat redup, serpihan sedikit), bukan jalannya pertandingan. Kait di game.js: Hazard.step(dt) di akhir step() (sebelum feelWatchKo), Hazard.draw(ctx, 'back'|'front') di draw() (tanda lantai sebelum petarung, efek sebelum drawSquadron/drawEffects), dan __game.hazardCtx (getter match, stage, time, seed, menu, busy, cpu; sound; cpuRead(); cpuDodge(dir, dur)). Semburat layar memakai elemen DOM #hz-glare (di atas kanvas, di bawah HUD). Dimatikan lewat Pengaturan ("Bahaya arena", bawaan Aktif, localStorage['konoha.hazard']) atau Hazard.enabled = false (sementara, tidak disimpan; untuk tantangan khusus); keduanya harus menyala. Uji manual: ?hazard=cepat di URL (bahaya tiap 3-4,5 dtk), Hazard.trigger() di konsol saat versus berjalan, Hazard.state(), Hazard.onEvent(fn) (jenis aba-aba dan kena). Catatan: statistik pertandingan menghitung damage dari selisih HP, jadi damage bahaya ikut tercatat sebagai diterima pemain atau dikeluarkan pemain; simulator keseimbangan (tests/bot.mjs, arena Bellora) ikut memainkan bahaya, set Hazard.enabled = false di sana bila ingin angka tanpa bahaya. Belum ada skenario otomatis dan belum dilihat di peramban oleh penulisnya (hanya node --check dan smoke).
  • 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).