From 3902dfbc11759f95fcf8718f7fd3eac3fc697c8a Mon Sep 17 00:00:00 2001 From: rizkyaz0 Date: Mon, 5 Oct 2026 14:46:55 +0700 Subject: [PATCH] Rekam ulang K.O. dan ekspor tangkapan layar (RENCANA 7.3 dan 7.4): modul record.js + record.css. Dua perekam MediaRecorder bergantian (canvas.captureStream, webm, ~30 fps) menyimpan 6-12 dtk terakhir, berhenti saat menu/jeda/tab tersembunyi dan membuang yang lama; layar hasil mendapat SIMPAN KLIP, TONTON ULANG, FOTO dan BAGIKAN lewat kait kecil di vs.js; tombol kamera HUD dan menu jeda, F9, PNG berwatermark, Web Share dengan cadangan unduh; baris Rekam ulang K.O. dan Indikator REC di Pengaturan. game.js tidak diubah --- docs/ENGINE_NOTES.md | 1 + index.html | 2 + record.css | 53 ++++++ record.js | 441 +++++++++++++++++++++++++++++++++++++++++++ vs.js | 6 +- 5 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 record.css create mode 100644 record.js diff --git a/docs/ENGINE_NOTES.md b/docs/ENGINE_NOTES.md index 2cd517b..05193cb 100644 --- a/docs/ENGINE_NOTES.md +++ b/docs/ENGINE_NOTES.md @@ -19,6 +19,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un - `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)`. +- Rekam ulang K.O. dan tangkapan layar (RENCANA 7.3 dan 7.4): `record.js` (`window.Record`) + `record.css`; tag di baris sendiri sebelum `` 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--vs-.webm`), TONTON ULANG (`` 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 `
Daftar gerakan
+ +
diff --git a/record.css b/record.css new file mode 100644 index 0000000..6b11c60 --- /dev/null +++ b/record.css @@ -0,0 +1,53 @@ +/* Rekam ulang K.O. dan ekspor tangkapan layar (record.js, RENCANA 7.3 dan 7.4): tombol kamera di HUD, indikator REC, baris di menu jeda, toolbar di layar hasil, dialog Tonton ulang. + Tinggi minimum yang didukung tetap 520 px: toolbar layar hasil melayang (absolute) di pojok kanan atas kolom tengah dan tidak menambah tinggi tata letak; dialog bisa digulir. */ + +.rec-ic{width:1.25em;height:1.25em;flex:none;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round} + +/* ---- HUD: tombol kamera memakai gaya .match-actions button (style.css); REC menempel di bawah barisnya tanpa mengubah tata letak ---- */ +#rec-shot.rec-done{border-color:#8dffa8;background:#1f5a45} +@media(max-width:620px){#rec-shot{display:none}} /* kolom tengah header terlalu sempit untuk tiga tombol; F9 dan tombol di menu jeda tetap ada */ +.rec-badge{position:absolute;top:calc(100% + 4px);left:50%;transform:translateX(-50%);display:inline-flex;align-items:center;gap:5px;padding:0 7px 0 6px;font:700 11px/15px Rajdhani,Arial,sans-serif;letter-spacing:1.6px;color:#ffd9d3;background:#1a0c0fcc;border:1px solid #ff6a5a88;pointer-events:none;white-space:nowrap} +.rec-badge[hidden]{display:none} +.rec-badge i{width:7px;height:7px;border-radius:50%;background:#ff4a3d;box-shadow:0 0 6px #ff4a3d;animation:rec-blink 1.4s ease-in-out infinite} +@keyframes rec-blink{50%{opacity:.3}} + +/* ---- Menu jeda ---- */ +.rec-btns{display:flex;gap:8px;margin-top:12px} +.rec-btns .rec-b{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:7px;min-width:0;border:1px solid #8da8b153;background:#274352;padding:8px;font-size:15px;font-weight:700;letter-spacing:.4px} +.rec-btns .rec-b:hover{background:#2f5365} +#pause-panel .rec-btns{margin-top:10px} + +/* ---- Layar hasil: toolbar di pojok kanan atas .rs-main (di atas kepala hasil, di pita kosong tempat konten diratakan ke tengah) ---- */ +.rs-rec{position:absolute;top:0;right:0;z-index:3;display:flex;flex-wrap:wrap;justify-content:flex-end;gap:5px;max-width:100%;animation:rs-up .5s 1s both} +.rs-rec .rec-b{display:inline-flex;align-items:center;gap:6px;margin:0;padding:clamp(4px,.9vh,7px) clamp(7px,1vw,11px);font:700 clamp(10px,1.3vh + 2px,13px)/1 Rajdhani,Arial,sans-serif;letter-spacing:1.5px;color:#cbddd4;background:#0b1c26d9;border:1px solid #8ba89d66;border-radius:0;cursor:pointer;white-space:nowrap} +.rs-rec .rec-b:hover:not([disabled]),.rs-rec .rec-b:focus-visible{background:#24505e;color:#fff} +.rs-rec .rec-b[disabled]{opacity:.5;cursor:default} +.rs-rec .rec-b[aria-busy=true] .rec-ic{animation:rec-pulse 1s ease-in-out infinite} +@keyframes rec-pulse{50%{opacity:.35}} +#front-menu .rs-rec .rec-b:focus-visible{outline-offset:-3px} +@media(max-width:700px),(max-height:420px){.rs-rec{gap:4px}.rs-rec .rec-b{padding:4px 7px;gap:5px;letter-spacing:.8px}} +@media(max-width:620px){.rs-rec .t{display:none}.rs-rec .rec-b{padding:6px 8px}} /* hanya ikon; nama tombol tetap ada lewat aria-label dan title */ +@media(pointer:coarse){.rs-rec .rec-b{min-height:32px}} + +/* ---- Umpan balik: toast kecil di bawah tengah dan kilat kamera ---- */ +.rec-toast{position:fixed;left:50%;bottom:max(16px,env(safe-area-inset-bottom));transform:translateX(-50%);z-index:120;max-width:min(92vw,520px);padding:8px 16px;background:#101923ee;border:1px solid #e8b84a;color:#f6eedb;font:700 16px/1.25 Rajdhani,Arial,sans-serif;letter-spacing:.05em;text-align:center;pointer-events:none;animation:rec-toast .2s ease-out both} +.rec-toast.bad{border-color:#ff8a7a;color:#ffd9d3} +@keyframes rec-toast{from{opacity:0;transform:translate(-50%,8px)}to{opacity:1;transform:translateX(-50%)}} +.rec-flash{position:fixed;inset:0;z-index:119;background:#fff;pointer-events:none;animation:rec-flash .3s ease-out both} +@keyframes rec-flash{from{opacity:.75}to{opacity:0}} + +/* ---- Tonton ulang ---- */ +#rec-replay{width:min(94vw,860px);max-width:calc(100% - 16px);padding:10px 14px 12px;overflow:auto} +#rec-replay .dialog-kicker{font-size:11px} +#rec-replay h2{font-size:22px;margin:2px 0 8px;letter-spacing:1px} +#rec-replay video{display:block;width:100%;height:auto;max-height:calc(100vh - 160px);background:#000;object-fit:contain;cursor:pointer} +.rp-bar{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px} +.rp-bar button{flex:1 1 88px;border:1px solid #8da8b153;background:#274352;padding:8px;font-size:15px;font-weight:700;letter-spacing:.4px} +.rp-bar button:hover{background:#2f5365} +.rp-bar button[data-rp=close]{border-color:transparent;background:var(--gold);color:#132b37} +.rp-bar button[data-rp=close]:hover{background:#fae0b4} +@media(max-height:480px){#rec-replay{padding:8px 12px}#rec-replay h2{font-size:18px;margin:0 0 6px}#rec-replay .dialog-kicker{display:none}#rec-replay video{max-height:calc(100vh - 112px)}.rp-bar{margin-top:6px}.rp-bar button{padding:6px;font-size:14px}} + +@media(prefers-reduced-motion:reduce){.rec-badge i,.rec-toast,.rs-rec .rec-b .rec-ic{animation:none!important}.rec-flash{display:none}} +html.acc-calm .rec-badge i,html.acc-calm .rec-toast,html.acc-calm .rs-rec .rec-b .rec-ic{animation:none!important} +html.acc-calm .rec-flash{display:none} diff --git a/record.js b/record.js new file mode 100644 index 0000000..0b09218 --- /dev/null +++ b/record.js @@ -0,0 +1,441 @@ +/* Rekam ulang K.O. dan ekspor tangkapan layar (RENCANA 7.3 dan 7.4). Gaya: record.css. Seluruh logika ada di berkas ini; game.js tidak diubah. + Kait di berkas lain: vs.js (ResultView.view memanggil window.Record?.resultHtml(hasil) di dalam .rs-main, ResultView.mount memanggil window.Record?.mount(root)) + dan satu baris /