Tambah uji regresi layar hasil dan VS: menu tidak menutupi arena setelah MAIN LAGI, Spasi/Enter tidak memulai ulang, toast tidak menutupi bilah VS, Statistik tidak meluap di layar pendek dan potret; catat di ENGINE_NOTES
This commit is contained in:
parent
13b8a49b12
commit
deb7df20d0
|
|
@ -18,7 +18,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un
|
||||||
- `touch.js`: kontrol sentuh; tombol memanggil `__game.input(key, down)`.
|
- `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`.
|
- `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).
|
- 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()`.
|
- `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.
|
||||||
- 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']`.
|
- 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']`.
|
||||||
- `assets/<id>/` atlas sprite (15 state), `manifest.js` (data + metrik), `ui/`; `work/` (di repo induk, luar folder ini) berisi pipeline pembuat karakter.
|
- `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.
|
- `precache.js`/`files.json`: **dibuat otomatis** oleh `tools/build_precache.py` — jangan diedit atau di-commit oleh cabang fitur; perintahkan integrator membangun ulang.
|
||||||
|
|
@ -29,7 +29,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un
|
||||||
`__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.
|
`__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
|
## 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|phone`) menguji layar VS, layar hasil, dan kutipan. `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.
|
`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. `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
|
## Aturan kerja
|
||||||
- Jangan memakai `Math.random`/`Date.now` untuk logika yang memengaruhi hasil pertandingan bila ada pengacak `random()` bawaan (benih tetap).
|
- Jangan memakai `Math.random`/`Date.now` untuk logika yang memengaruhi hasil pertandingan bila ada pengacak `random()` bawaan (benih tetap).
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Tahap 2.2, 2.3, 2.6: layar VS, layar hasil, kutipan.
|
// Tahap 2.2, 2.3, 2.6: layar VS, layar hasil, kutipan.
|
||||||
// node tests/harness.mjs tests/t2-vsresult.mjs
|
// node tests/harness.mjs tests/t2-vsresult.mjs
|
||||||
// VSR_SHOTS=folder simpan screenshot (default: folder sementara sistem); VSR_ONLY=regex hanya bagian yang namanya cocok (quotes|vs|skip|abort|result|run|ui|phone)
|
// VSR_SHOTS=folder simpan screenshot (default: folder sementara sistem); VSR_ONLY=regex hanya bagian yang namanya cocok (quotes|vs|skip|abort|result|run|ui|focus|phone)
|
||||||
// Jendela 1280x520 dipakai sengaja: tampilan harus muat di tinggi 520 px.
|
// Jendela 1280x520 dipakai sengaja: tampilan harus muat di tinggi 520 px.
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
|
|
@ -24,6 +24,10 @@ export default async ({ page, game, errors, characters }) => {
|
||||||
}, { side, rounds });
|
}, { side, rounds });
|
||||||
const startVS = (p, e, extra = {}) => game(({ p, e, extra }) => { const g = window.__game; g.manual = false; return g.startMatch({ player: p, enemy: e, stage: extra.stage || 'bellora', level: extra.level || 'medium', mode: extra.mode || 'versus', vs: extra.vs ?? true }); }, { p, e, extra });
|
const startVS = (p, e, extra = {}) => game(({ p, e, extra }) => { const g = window.__game; g.manual = false; return g.startMatch({ player: p, enemy: e, stage: extra.stage || 'bellora', level: extra.level || 'medium', mode: extra.mode || 'versus', vs: extra.vs ?? true }); }, { p, e, extra });
|
||||||
const vsState = () => game(() => { const g = window.__game, el = document.querySelector('#vs-screen'); const s = g.snapshot(); return { el: !!el, out: el?.classList.contains('out') || false, active: g.vsActive(), phase: s.match?.phase, phaseTime: +(s.match?.phaseTime || 0).toFixed(3), cues: s.roundCues.map(c => c.cue + c.round), paused: s.paused, menuOpen: s.menuOpen, names: [...document.querySelectorAll('#vs-screen .vs-name')].map(n => n.textContent), quotes: [...document.querySelectorAll('#vs-screen .vs-quote')].map(n => n.textContent), roundCard: !document.querySelector('#round-call').hidden }; });
|
const vsState = () => game(() => { const g = window.__game, el = document.querySelector('#vs-screen'); const s = g.snapshot(); return { el: !!el, out: el?.classList.contains('out') || false, active: g.vsActive(), phase: s.match?.phase, phaseTime: +(s.match?.phaseTime || 0).toFixed(3), cues: s.roundCues.map(c => c.cue + c.round), paused: s.paused, menuOpen: s.menuOpen, names: [...document.querySelectorAll('#vs-screen .vs-name')].map(n => n.textContent), quotes: [...document.querySelectorAll('#vs-screen .vs-quote')].map(n => n.textContent), roundCard: !document.querySelector('#round-call').hidden }; });
|
||||||
|
// Menu depan benar-benar hilang (bukan hanya atribut hidden) dan tidak menutupi arena; toast yang tampil dan perpotongannya dengan bilah atas layar VS.
|
||||||
|
const menuGone = () => game(() => { const fm = document.querySelector('#front-menu'), hit = document.elementFromPoint(innerWidth / 2, innerHeight / 2); return { hidden: fm.hidden, display: getComputedStyle(fm).display, screen: fm.dataset.screen, coversArena: !!hit?.closest('#front-menu'), hit: hit?.id || hit?.className || hit?.tagName, vs: !!document.querySelector('#vs-screen') }; });
|
||||||
|
const arenaClear = (name, m) => ok(name, m.hidden && m.display === 'none' && !m.coversArena && !m.vs, m);
|
||||||
|
const toasts = () => game(() => { const top = document.querySelector('#vs-screen .vs-top')?.getBoundingClientRect(); return [...document.querySelectorAll('.meta-toast')].map(t => { const r = t.getBoundingClientRect(); return { text: t.textContent, opacity: getComputedStyle(t).opacity, coversVsTop: !!top && r.bottom > top.top && r.top < top.bottom && r.right > top.left && r.left < top.right }; }); });
|
||||||
|
|
||||||
// ---------- 2.6 data kutipan ----------
|
// ---------- 2.6 data kutipan ----------
|
||||||
if (want('quotes')) {
|
if (want('quotes')) {
|
||||||
|
|
@ -185,6 +189,8 @@ export default async ({ page, game, errors, characters }) => {
|
||||||
await wait(150); let s = await vsState();
|
await wait(150); let s = await vsState();
|
||||||
const label = await game(() => document.querySelector('#vs-screen .vs-top')?.textContent || '');
|
const label = await game(() => document.querySelector('#vs-screen .vs-top')?.textContent || '');
|
||||||
ok('arcade: lawan 1 memakai layar VS berlabel', s.el && s.active && /ARCADE · LAWAN 1 \/ 6/.test(label), { s, label });
|
ok('arcade: lawan 1 memakai layar VS berlabel', s.el && s.active && /ARCADE · LAWAN 1 \/ 6/.test(label), { s, label });
|
||||||
|
const tArc = await toasts();
|
||||||
|
ok('arcade: tidak ada toast di atas bilah layar VS (info lawan sudah ada di bilah)', tArc.length === 0, tArc);
|
||||||
await wait(450); await shot('vs-arcade'); await game(() => window.__game.vsSkip());
|
await wait(450); await shot('vs-arcade'); await game(() => window.__game.vsSkip());
|
||||||
await finish('player', 2).then(() => null).catch(() => null);
|
await finish('player', 2).then(() => null).catch(() => null);
|
||||||
await wait(100);
|
await wait(100);
|
||||||
|
|
@ -202,7 +208,9 @@ export default async ({ page, game, errors, characters }) => {
|
||||||
await page.click('[data-cmd=rematch]'); await wait(200); s = await vsState();
|
await page.click('[data-cmd=rematch]'); await wait(200); s = await vsState();
|
||||||
const lab2 = await game(() => document.querySelector('#vs-screen .vs-top')?.textContent || '');
|
const lab2 = await game(() => document.querySelector('#vs-screen .vs-top')?.textContent || '');
|
||||||
ok('arcade: Main lagi memulai rangkaian baru (lawan 1)', s.el && /LAWAN 1 \/ 6/.test(lab2), { s, lab2 });
|
ok('arcade: Main lagi memulai rangkaian baru (lawan 1)', s.el && /LAWAN 1 \/ 6/.test(lab2), { s, lab2 });
|
||||||
await game(() => window.__game.vsSkip()); await finish('enemy'); await wait(200);
|
await game(() => window.__game.vsSkip()); await wait(350);
|
||||||
|
arenaClear('arcade: Main lagi, setelah VS layar hasil tidak lagi menutupi arena', await menuGone());
|
||||||
|
await finish('enemy'); await wait(200);
|
||||||
await page.click('[data-cmd=reselect]'); await wait(100);
|
await page.click('[data-cmd=reselect]'); await wait(100);
|
||||||
const sel = await game(() => ({ screen: document.querySelector('#front-menu').dataset.screen, step: document.querySelector('#front-menu').dataset.step, eyebrow: document.querySelector('.select-header .eyebrow')?.textContent }));
|
const sel = await game(() => ({ screen: document.querySelector('#front-menu').dataset.screen, step: document.querySelector('#front-menu').dataset.step, eyebrow: document.querySelector('.select-header .eyebrow')?.textContent }));
|
||||||
ok('arcade: Pilih ulang kembali ke pilih karakter (mode ARCADE)', sel.screen === 'select' && sel.step === '0' && /ARCADE/.test(sel.eyebrow), sel);
|
ok('arcade: Pilih ulang kembali ke pilih karakter (mode ARCADE)', sel.screen === 'select' && sel.step === '0' && /ARCADE/.test(sel.eyebrow), sel);
|
||||||
|
|
@ -225,6 +233,7 @@ export default async ({ page, game, errors, characters }) => {
|
||||||
// ---------- jalur menu: dua pemain, tombol hasil ----------
|
// ---------- jalur menu: dua pemain, tombol hasil ----------
|
||||||
if (want('ui')) {
|
if (want('ui')) {
|
||||||
await game(() => { window.Meta.save.ach = { first_win: 1, perfect: 1, hard_win: 1, combo5: 1, combo10: 1, wins10: 1, rich: 1 }; window.Meta.cancelRun(); window.__game.setMenuOpen(true); window.FrontEnd.home(); });
|
await game(() => { window.Meta.save.ach = { first_win: 1, perfect: 1, hard_win: 1, combo5: 1, combo10: 1, wins10: 1, rich: 1 }; window.Meta.cancelRun(); window.__game.setMenuOpen(true); window.FrontEnd.home(); });
|
||||||
|
await game(() => { delete window.Meta.save.ach.twoplayer; });
|
||||||
await page.click('[data-cmd="2p"]'); await wait(80);
|
await page.click('[data-cmd="2p"]'); await wait(80);
|
||||||
await page.click('[data-fighter=browo]'); await wait(1100);
|
await page.click('[data-fighter=browo]'); await wait(1100);
|
||||||
await page.click('[data-fighter=megachan]'); await wait(1100);
|
await page.click('[data-fighter=megachan]'); await wait(1100);
|
||||||
|
|
@ -232,7 +241,11 @@ export default async ({ page, game, errors, characters }) => {
|
||||||
let s = await vsState();
|
let s = await vsState();
|
||||||
const meta = await game(() => ({ who: [...document.querySelectorAll('#vs-screen .vs-who')].map(n => n.textContent), top: document.querySelector('#vs-screen .vs-top')?.textContent }));
|
const meta = await game(() => ({ who: [...document.querySelectorAll('#vs-screen .vs-who')].map(n => n.textContent), top: document.querySelector('#vs-screen .vs-top')?.textContent }));
|
||||||
ok('dua pemain lewat menu: layar VS berlabel PEMAIN 1 / PEMAIN 2', s.active && meta.who.join() === 'PEMAIN 1,PEMAIN 2' && /DUA PEMAIN/.test(meta.top), { s, meta });
|
ok('dua pemain lewat menu: layar VS berlabel PEMAIN 1 / PEMAIN 2', s.active && meta.who.join() === 'PEMAIN 1,PEMAIN 2' && /DUA PEMAIN/.test(meta.top), { s, meta });
|
||||||
|
const tDuring = await toasts();
|
||||||
|
ok('dua pemain: toast petunjuk P2 dan pencapaian ditunda, tidak menutupi bilah VS', tDuring.length === 0, tDuring);
|
||||||
await wait(2200); s = await vsState();
|
await wait(2200); s = await vsState();
|
||||||
|
const tAfter = await toasts();
|
||||||
|
ok('dua pemain: petunjuk P2 dan pencapaian TEMAN BERMAIN muncul tepat setelah VS selesai', tAfter.some(t => /^P2:/.test(t.text)) && tAfter.some(t => /TEMAN BERMAIN/.test(t.text)) && tAfter.every(t => !t.coversVsTop), tAfter);
|
||||||
ok('dua pemain: ronde mulai normal setelah VS', !s.active && s.cues.join() === 'round1', s);
|
ok('dua pemain: ronde mulai normal setelah VS', !s.active && s.cues.join() === 'round1', s);
|
||||||
await finish('enemy'); await wait(150);
|
await finish('enemy'); await wait(150);
|
||||||
const two = await game(() => ({ screen: document.querySelector('#front-menu').dataset.screen, kicker: document.querySelector('.rs-kicker')?.textContent, tone: document.querySelector('.rs')?.dataset.tone }));
|
const two = await game(() => ({ screen: document.querySelector('#front-menu').dataset.screen, kicker: document.querySelector('.rs-kicker')?.textContent, tone: document.querySelector('.rs')?.dataset.tone }));
|
||||||
|
|
@ -247,7 +260,9 @@ export default async ({ page, game, errors, characters }) => {
|
||||||
await page.click('[data-cmd=rematch]'); await wait(200); s = await vsState();
|
await page.click('[data-cmd=rematch]'); await wait(200); s = await vsState();
|
||||||
const again = await game(() => ({ who: [...document.querySelectorAll('#vs-screen .vs-who')].map(n => n.textContent) }));
|
const again = await game(() => ({ who: [...document.querySelectorAll('#vs-screen .vs-who')].map(n => n.textContent) }));
|
||||||
ok('tombol MAIN LAGI memulai ulang (VS tampil, tetap dua pemain)', s.active && again.who.join() === 'PEMAIN 1,PEMAIN 2', { s, again });
|
ok('tombol MAIN LAGI memulai ulang (VS tampil, tetap dua pemain)', s.active && again.who.join() === 'PEMAIN 1,PEMAIN 2', { s, again });
|
||||||
await game(() => window.__game.vsSkip()); await finish('player'); await wait(150);
|
await game(() => window.__game.vsSkip()); await wait(350);
|
||||||
|
arenaClear('MAIN LAGI (dua pemain): setelah VS layar hasil tidak lagi menutupi arena', await menuGone());
|
||||||
|
await finish('player'); await wait(150);
|
||||||
await page.click('[data-cmd=home]'); await wait(100);
|
await page.click('[data-cmd=home]'); await wait(100);
|
||||||
m = await game(() => ({ screen: document.querySelector('#front-menu').dataset.screen }));
|
m = await game(() => ({ screen: document.querySelector('#front-menu').dataset.screen }));
|
||||||
ok('tombol MENU UTAMA kembali ke menu utama', m.screen === 'home', m);
|
ok('tombol MENU UTAMA kembali ke menu utama', m.screen === 'home', m);
|
||||||
|
|
@ -259,6 +274,36 @@ export default async ({ page, game, errors, characters }) => {
|
||||||
const cpu = await game(() => ({ who: [...document.querySelectorAll('#vs-screen .vs-who')].map(n => n.textContent), top: document.querySelector('#vs-screen .vs-top')?.textContent }));
|
const cpu = await game(() => ({ who: [...document.querySelectorAll('#vs-screen .vs-who')].map(n => n.textContent), top: document.querySelector('#vs-screen .vs-top')?.textContent }));
|
||||||
ok('versus CPU lewat menu: label KOMPUTER, arena, tingkat SULIT', cpu.who.join() === 'PEMAIN 1,KOMPUTER' && /VERSUS KOMPUTER/.test(cpu.top) && /SULIT/.test(cpu.top), cpu);
|
ok('versus CPU lewat menu: label KOMPUTER, arena, tingkat SULIT', cpu.who.join() === 'PEMAIN 1,KOMPUTER' && /VERSUS KOMPUTER/.test(cpu.top) && /SULIT/.test(cpu.top), cpu);
|
||||||
await shot('vs-versus-menu'); await game(() => window.__game.vsSkip());
|
await shot('vs-versus-menu'); await game(() => window.__game.vsSkip());
|
||||||
|
await finish('player'); await wait(150);
|
||||||
|
await page.click('[data-cmd=rematch]'); await wait(200); await game(() => window.__game.vsSkip()); await wait(350);
|
||||||
|
arenaClear('MAIN LAGI (versus CPU): setelah VS layar hasil tidak lagi menutupi arena', await menuGone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- fokus awal layar hasil: Spasi/Enter (tombol serang) tidak boleh langsung memulai ulang ----------
|
||||||
|
if (want('focus')) {
|
||||||
|
await game(() => { window.Meta.cancelRun(); window.Meta.save.ach = { first_win: 1, perfect: 1, hard_win: 1, combo5: 1, combo10: 1, wins10: 1, rich: 1, arcade_clear: 1, survival5: 1 }; window.__game.manual = false; });
|
||||||
|
const state = () => game(() => { const g = window.__game, a = document.activeElement; return { menuOpen: g.snapshot().menuOpen, screen: document.querySelector('#front-menu').dataset.screen, rs: !!document.querySelector('.rs'), vs: !!document.querySelector('#vs-screen'), activeCmd: a?.dataset?.cmd || null, activeTag: a?.tagName }; });
|
||||||
|
const stays = (s) => s.menuOpen && s.screen === 'result' && s.rs && !s.vs;
|
||||||
|
await startVS('browo', 'megachan', { vs: false, level: 'easy' });
|
||||||
|
await finish('player'); await wait(150);
|
||||||
|
let s = await state();
|
||||||
|
ok('hasil: fokus awal bukan tombol aksi (bukan MAIN LAGI)', stays(s) && s.activeCmd === null, s);
|
||||||
|
await page.keyboard.press(' '); await page.keyboard.press('Enter'); await wait(450); s = await state();
|
||||||
|
ok('hasil: Spasi lalu Enter tepat setelah layar muncul tidak memulai ulang', stays(s), s);
|
||||||
|
// tombol serang ditahan menembus K.O.: auto-repeat dan keyup tidak boleh mengaktifkan tombol apa pun
|
||||||
|
await page.keyboard.down(' '); await page.keyboard.down(' '); await page.keyboard.down(' '); await wait(60); await page.keyboard.up(' '); await wait(450); s = await state();
|
||||||
|
ok('hasil: Spasi ditahan (auto-repeat) lalu dilepas tidak memulai ulang', stays(s), s);
|
||||||
|
await page.keyboard.press('Tab'); s = await state();
|
||||||
|
ok('hasil: Tab menjangkau MAIN LAGI (papan ketik tetap bisa dipakai)', s.activeCmd === 'rematch', s);
|
||||||
|
await page.keyboard.press('Enter'); await wait(250); s = await state();
|
||||||
|
ok('hasil: Tab lalu Enter (sengaja) memulai ulang dengan layar VS', !s.menuOpen && s.vs, s);
|
||||||
|
await game(() => window.__game.vsSkip());
|
||||||
|
// akhir Arcade: Spasi tidak boleh memulai rangkaian baru
|
||||||
|
await game(() => { window.Meta.startRun('arcade', 'browo'); }); await wait(100); await game(() => window.__game.vsSkip());
|
||||||
|
await finish('enemy'); await wait(200);
|
||||||
|
await page.keyboard.press(' '); await page.keyboard.press('Enter'); await wait(450); s = await state();
|
||||||
|
ok('akhir Arcade: Spasi/Enter tidak memulai rangkaian baru', stays(s) && await game(() => document.querySelector('.rs-kicker')?.textContent.includes('KALAH DI LAWAN')), s);
|
||||||
|
await game(() => { window.Meta.cancelRun(); window.__game.setMenuOpen(true); window.FrontEnd.home(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- ponsel lanskap ----------
|
// ---------- ponsel lanskap ----------
|
||||||
|
|
@ -270,6 +315,28 @@ export default async ({ page, game, errors, characters }) => {
|
||||||
await finish('player'); await wait(2300); await shot('hasil-667x375');
|
await finish('player'); await wait(2300); await shot('hasil-667x375');
|
||||||
const fit = await game(() => { const b = [...document.querySelectorAll('.rs-actions button')].map(x => x.getBoundingClientRect()); return { bottom: Math.max(...b.map(x => x.bottom)), h: innerHeight, scroll: document.querySelector('#front-menu').scrollHeight - document.querySelector('#front-menu').clientHeight }; });
|
const fit = await game(() => { const b = [...document.querySelectorAll('.rs-actions button')].map(x => x.getBoundingClientRect()); return { bottom: Math.max(...b.map(x => x.bottom)), h: innerHeight, scroll: document.querySelector('#front-menu').scrollHeight - document.querySelector('#front-menu').clientHeight }; });
|
||||||
ok('hasil di ponsel lanskap 667x375: tombol muat tanpa gulir', fit.bottom <= fit.h && fit.scroll <= 0, fit);
|
ok('hasil di ponsel lanskap 667x375: tombol muat tanpa gulir', fit.bottom <= fit.h && fit.scroll <= 0, fit);
|
||||||
|
// Kolom Statistik tidak boleh meluap atau terpotong di layar pendek maupun potret (gerak dikurangi: tata letak langsung jadi, tanpa menunggu animasi).
|
||||||
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||||
|
const stats = () => game(() => { const p = document.querySelector('.rs-stats'), pr = p.getBoundingClientRect(), bad = []; for (const n of p.querySelectorAll('dt,dd')) { const r = n.getBoundingClientRect(); if (r.right > pr.right + .5 || r.right > innerWidth + .5 || n.scrollWidth > n.clientWidth + 1) bad.push(`${n.textContent} r=${Math.round(r.right)} sw=${n.scrollWidth} cw=${n.clientWidth}`); } const ab = Math.max(...[...document.querySelectorAll('.rs-actions button')].map(x => x.getBoundingClientRect().bottom)); return { bad, panelRight: Math.round(pr.right), panelOverflow: p.scrollWidth - p.clientWidth, vw: innerWidth, actionsBottom: Math.round(ab), vh: innerHeight }; });
|
||||||
|
const base = { winner: 'player', playerWins: 2, enemyWins: 0, player: 'sonofsolo', enemy: 'ethanolman', level: 'excellent', mode: 'versus', bestCombo: 14, matchBest: 14, award: { won: true, gain: 120, lines: [{ key: 'win', label: 'Menang', amount: 120 }] } };
|
||||||
|
const variants = [
|
||||||
|
['versus elderwood', { stage: 'elderwood' }], ['versus sunspire', { stage: 'sunspire' }],
|
||||||
|
['arcade kalah', { winner: 'enemy', playerWins: 0, enemyWins: 2, stage: 'elderwood', run: { kind: 'arcade', wins: 1, total: 6, cleared: false, best: 3 } }],
|
||||||
|
['survival kalah', { winner: 'enemy', playerWins: 0, enemyWins: 2, stage: 'sunspire', run: { kind: 'survival', wins: 12, total: 0, cleared: false, best: 14 } }]
|
||||||
|
];
|
||||||
|
const bads = [], lows = [];
|
||||||
|
for (const [w, h] of [[568, 320], [667, 375], [740, 360], [812, 375], [844, 390], [896, 414], [1024, 520], [768, 1024]]) {
|
||||||
|
await page.setViewportSize({ width: w, height: h });
|
||||||
|
for (const [name, extra] of variants) {
|
||||||
|
await game(d => { window.__game.setMenuOpen(true); window.FrontEnd.showResult(d); }, { ...base, ...extra });
|
||||||
|
await wait(120);
|
||||||
|
const st = await stats(); if (st.bad.length || st.panelOverflow > 0) bads.push({ viewport: `${w}x${h}`, variant: name, ...st }); if (st.actionsBottom > st.vh) lows.push(`${w}x${h} ${name} bottom=${st.actionsBottom}`);
|
||||||
|
if (w === 667 && name === 'arcade kalah') await shot('hasil-arcade-667x375');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok('hasil: kolom Statistik tidak meluap atau terpotong di 568x320 sampai 1024x520 dan potret 768x1024 (versus, Arcade, Survival)', bads.length === 0, bads.slice(0, 4));
|
||||||
|
ok('hasil: tombol aksi tetap muat di semua ukuran uji (Statistik tidak menambah tinggi berlebih)', lows.length === 0, lows);
|
||||||
|
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
||||||
await page.setViewportSize({ width: 1280, height: 520 });
|
await page.setViewportSize({ width: 1280, height: 520 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue