S1 fix real: deblocare audio pe primul gest (acopera resume), nu doar btn-start
Fix-ul initial deblocà AudioContext-ul doar in handlerul btn-start. Lacuna: calea de resume (reload mid-campanie) intra direct pe harta fara btn-start -> ctx nedeblocat -> camere mute. Plus resume() singur nu ajunge pe iOS Safari. - unlockAudio() + listener global one-time (pointerdown+keydown capture): acopera fresh SI resume; buffer silentios iOS-safe. - beep() se auto-vindeca daca ctx redevine suspended. - Test smoke #9 rescris: headless creeaza ctx direct 'running' (ignora autoplay) -> vechiul "ctx running" trecea trivial. Acum: gest tastatura fara btn-start -> running (cale resume) + beep self-heal din ctx suspendat. - Demo campanie regenerat. Suita 24/24. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
19
TODOS.md
19
TODOS.md
@@ -16,13 +16,18 @@ Direcția cerută de user (decizii confirmate, vezi `HANDOFF.md`). Model hibrid
|
||||
părțile grele se prototipează în PARALEL în `scratch/`, verificate jucabile, apoi integrator le
|
||||
portează în `escape-builder.html` (un singur fișier, integrare secvențială).
|
||||
|
||||
- [x] **S1 — fix sunet campanie** *(GATA, verificat în browser)*
|
||||
Cauză reală: orchestratorul crea `beep._ctx` lazy la primul `parent.beep()` din iframe;
|
||||
gestul din iframe NU deblochează AudioContext-ul părintelui → ctx `suspended` → tăcere.
|
||||
(Ipoteza HANDOFF „beep nedefinit" era greșită; `beep` e la `escape-builder.html:1725`.)
|
||||
Fix: deblocare ctx în handler-ul `btn-start` (gest direct pe părinte), `escape-builder.html:1928`.
|
||||
Verificat: `scratch/verify-audio-s1.mjs` → ctx `running` după start (era `NO_CTX`). Smoke 21/21.
|
||||
TODO la S4: portează asertarea `beep._ctx.state==='running'` în `tests/smoke.mjs`.
|
||||
- [x] **S1 — fix sunet campanie** *(GATA — REVENIT: fix-ul inițial era incomplet, user raporta tăcere)*
|
||||
Cauză reală: gestul din iframe NU deblochează AudioContext-ul părintelui → ctx `suspended` → tăcere.
|
||||
Fix v1 (incomplet): deblocare DOAR în handler-ul `btn-start`. Lacună: calea de **resume**
|
||||
(reload mid-campanie, `escape-builder.html:2199`) intră direct pe hartă FĂRĂ btn-start → ctx
|
||||
nedeblocat → camere mute. Plus `resume()` singur nu ajunge pe iOS Safari.
|
||||
Fix v2 (real): `unlockAudio()` + listener GLOBAL one-time pe primul gest (`pointerdown`+`keydown`,
|
||||
capture) — acoperă fresh ȘI resume (mers pe hartă = keydown pe părinte); buffer silențios
|
||||
iOS-safe; `beep()` se auto-vindecă dacă ctx redevine `suspended`. `escape-builder.html:1893`.
|
||||
**Lecție testare:** headless Chromium creează ctx direct `running` (ignoră autoplay policy) →
|
||||
vechiul test „ctx running" trecea trivial, NU putea prinde tăcerea. Test nou (smoke #9):
|
||||
gest tastatură FĂRĂ btn-start → running (cale resume) + beep self-heal din ctx suspendat.
|
||||
Verificat: smoke 24/24 + live MCP (ArrowDown singur deblochează). Demo-uri regenerate.
|
||||
- [x] **S2a — prototip Bomberman complet** → `scratch/bomberman-proto.html` (GATA, 8/8 verificat de mine)
|
||||
Grid 15×13, bombe timer 2.4s + explozii lanț, cutii distructibile, AI dușmani BFS urmărire,
|
||||
3 vieți + respawn cu progres puzzle PĂSTRAT (stare separată), PRNG seedat (`window.__seed`),
|
||||
|
||||
@@ -1878,6 +1878,7 @@ function doorHtml(style, isLast, isStuck){
|
||||
function beep(ok){
|
||||
try{
|
||||
var ctx=beep._ctx||(beep._ctx=new(window.AudioContext||window.webkitAudioContext)());
|
||||
if(ctx.state==='suspended') ctx.resume(); /* safety: ctx poate fi suspendat din nou */
|
||||
var t=ctx.currentTime; var fs=ok?[523,784]:[196];
|
||||
fs.forEach(function(f,k){
|
||||
var o=ctx.createOscillator(),g=ctx.createGain();
|
||||
@@ -1890,6 +1891,28 @@ function beep(ok){
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
/* ----- Deblocare audio (D2) — primul gest pe părinte creează+deblochează ctx-ul.
|
||||
Necesar pe TOATE căile, nu doar btn-start: la resume (reload mid-campanie) se intră
|
||||
direct pe hartă fără btn-start, iar camerele cheamă parent.beep() din iframe (gestul
|
||||
din iframe NU deblochează ctx-ul părintelui). Pe iOS Safari resume() singur nu ajunge
|
||||
→ redăm și un buffer silențios în gest. Listener one-time, se auto-elimină. */
|
||||
function unlockAudio(){
|
||||
try{
|
||||
var c=beep._ctx||(beep._ctx=new(window.AudioContext||window.webkitAudioContext)());
|
||||
if(c.state==='suspended') c.resume();
|
||||
var b=c.createBuffer(1,1,22050),s=c.createBufferSource();
|
||||
s.buffer=b; s.connect(c.destination); s.start(0);
|
||||
}catch(e){}
|
||||
}
|
||||
var _audioUnlocked=false;
|
||||
function _onFirstGesture(){
|
||||
if(_audioUnlocked) return; _audioUnlocked=true; unlockAudio();
|
||||
document.removeEventListener('pointerdown',_onFirstGesture,true);
|
||||
document.removeEventListener('keydown',_onFirstGesture,true);
|
||||
}
|
||||
document.addEventListener('pointerdown',_onFirstGesture,true);
|
||||
document.addEventListener('keydown',_onFirstGesture,true);
|
||||
|
||||
/* ----- parent.* API ----- */
|
||||
|
||||
window.nextRoom = function(data){
|
||||
@@ -2170,9 +2193,7 @@ document.getElementById('intro-title').textContent = MASTER.title;
|
||||
document.getElementById('intro-story').textContent = (MASTER.player?'Salut, '+MASTER.player+'! ':'')+MASTER.story;
|
||||
document.getElementById('intro-promise').textContent = N+' camere \\u00b7 ${nStyles} stiluri \\u00b7 1 cuvânt magic';
|
||||
document.getElementById('btn-start').onclick = function(){
|
||||
/* Deblochează AudioContext-ul AICI (gest direct pe părinte) — camerele cheamă
|
||||
parent.beep() din iframe, iar gestul din iframe NU deblochează ctx-ul părintelui. */
|
||||
try{ var c=beep._ctx||(beep._ctx=new(window.AudioContext||window.webkitAudioContext)()); if(c.state==='suspended') c.resume(); }catch(e){}
|
||||
unlockAudio(); /* gest direct pe părinte (handlerul global prinde și el, dar e idempotent) */
|
||||
clearProgress(); owResetPlayer(); showOverworld(0);
|
||||
};
|
||||
|
||||
|
||||
@@ -312,6 +312,7 @@ function doorHtml(style, isLast, isStuck){
|
||||
function beep(ok){
|
||||
try{
|
||||
var ctx=beep._ctx||(beep._ctx=new(window.AudioContext||window.webkitAudioContext)());
|
||||
if(ctx.state==='suspended') ctx.resume(); /* safety: ctx poate fi suspendat din nou */
|
||||
var t=ctx.currentTime; var fs=ok?[523,784]:[196];
|
||||
fs.forEach(function(f,k){
|
||||
var o=ctx.createOscillator(),g=ctx.createGain();
|
||||
@@ -324,6 +325,28 @@ function beep(ok){
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
/* ----- Deblocare audio (D2) — primul gest pe părinte creează+deblochează ctx-ul.
|
||||
Necesar pe TOATE căile, nu doar btn-start: la resume (reload mid-campanie) se intră
|
||||
direct pe hartă fără btn-start, iar camerele cheamă parent.beep() din iframe (gestul
|
||||
din iframe NU deblochează ctx-ul părintelui). Pe iOS Safari resume() singur nu ajunge
|
||||
→ redăm și un buffer silențios în gest. Listener one-time, se auto-elimină. */
|
||||
function unlockAudio(){
|
||||
try{
|
||||
var c=beep._ctx||(beep._ctx=new(window.AudioContext||window.webkitAudioContext)());
|
||||
if(c.state==='suspended') c.resume();
|
||||
var b=c.createBuffer(1,1,22050),s=c.createBufferSource();
|
||||
s.buffer=b; s.connect(c.destination); s.start(0);
|
||||
}catch(e){}
|
||||
}
|
||||
var _audioUnlocked=false;
|
||||
function _onFirstGesture(){
|
||||
if(_audioUnlocked) return; _audioUnlocked=true; unlockAudio();
|
||||
document.removeEventListener('pointerdown',_onFirstGesture,true);
|
||||
document.removeEventListener('keydown',_onFirstGesture,true);
|
||||
}
|
||||
document.addEventListener('pointerdown',_onFirstGesture,true);
|
||||
document.addEventListener('keydown',_onFirstGesture,true);
|
||||
|
||||
/* ----- parent.* API ----- */
|
||||
|
||||
window.nextRoom = function(data){
|
||||
@@ -604,9 +627,7 @@ document.getElementById('intro-title').textContent = MASTER.title;
|
||||
document.getElementById('intro-story').textContent = (MASTER.player?'Salut, '+MASTER.player+'! ':'')+MASTER.story;
|
||||
document.getElementById('intro-promise').textContent = N+' camere \u00b7 3 stiluri \u00b7 1 cuvânt magic';
|
||||
document.getElementById('btn-start').onclick = function(){
|
||||
/* Deblochează AudioContext-ul AICI (gest direct pe părinte) — camerele cheamă
|
||||
parent.beep() din iframe, iar gestul din iframe NU deblochează ctx-ul părintelui. */
|
||||
try{ var c=beep._ctx||(beep._ctx=new(window.AudioContext||window.webkitAudioContext)()); if(c.state==='suspended') c.resume(); }catch(e){}
|
||||
unlockAudio(); /* gest direct pe părinte (handlerul global prinde și el, dar e idempotent) */
|
||||
clearProgress(); owResetPlayer(); showOverworld(0);
|
||||
};
|
||||
|
||||
|
||||
@@ -945,27 +945,48 @@ test.describe('Campanie E2E @campanie', () => {
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Test 9 (S4): Audio — AudioContext deblocat la "Incepe aventura" (S1)
|
||||
// Test 9 (S4+): Audio — deblocare ctx pe PRIMUL gest (orice), nu doar btn-start.
|
||||
// NB: headless Chromium creeaza AudioContext direct 'running' (ignora autoplay
|
||||
// policy), deci "ctx==running" e trivial. Testam WIRING-ul real al deblocarii:
|
||||
// (A) primul gest oarecare (tastatura, ca la resume) deblocheaza fara btn-start;
|
||||
// (B) beep() se auto-vindeca dintr-un ctx readus in 'suspended'.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
test('audio — AudioContext deblocat la Incepe aventura (S1) @campanie',
|
||||
test('audio — deblocare pe primul gest + beep self-heal (S1) @campanie',
|
||||
async ({ page }) => {
|
||||
const cfg = campaignCfg(3, 'classic');
|
||||
const tmpPath = await writeCampaignHtml(page, cfg, 'audio');
|
||||
const gp = await page.context().newPage();
|
||||
try {
|
||||
await gp.goto('file://' + tmpPath);
|
||||
// Inainte de gest: ctx inexistent (creat lazy)
|
||||
// Inainte de orice gest: ctx inexistent (creat lazy)
|
||||
const before = await gp.evaluate(
|
||||
() => (window.beep && window.beep._ctx) ? window.beep._ctx.state : 'NO_CTX'
|
||||
);
|
||||
expect(before, 'ctx nu trebuie sa existe inainte de gest').toBe('NO_CTX');
|
||||
// Gestul pe parinte deblocheaza ctx-ul
|
||||
await gp.locator('#btn-start').click();
|
||||
await gp.waitForTimeout(200);
|
||||
const after = await gp.evaluate(
|
||||
|
||||
// (A) Cale RESUME: un gest de tastatura (mers pe harta), FARA btn-start,
|
||||
// trebuie sa creeze+deblocheze ctx-ul prin listenerul global one-time.
|
||||
await gp.keyboard.press('ArrowDown');
|
||||
await gp.waitForTimeout(100);
|
||||
const afterKey = await gp.evaluate(
|
||||
() => (window.beep && window.beep._ctx) ? window.beep._ctx.state : 'NO_CTX'
|
||||
);
|
||||
expect(after, 'ctx trebuie running dupa Incepe aventura').toBe('running');
|
||||
expect(afterKey, 'gestul de tastatura trebuie sa deblocheze ctx (cale resume)').toBe('running');
|
||||
|
||||
// (B) beep() trebuie sa produca oscilatoare si sa reia un ctx suspendat.
|
||||
const heal = await gp.evaluate(async () => {
|
||||
await window.beep._ctx.suspend();
|
||||
const mid = window.beep._ctx.state; // 'suspended'
|
||||
let osc = 0;
|
||||
const orig = window.beep._ctx.createOscillator.bind(window.beep._ctx);
|
||||
window.beep._ctx.createOscillator = function () { osc++; return orig(); };
|
||||
window.beep(true);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
return { mid, osc, end: window.beep._ctx.state };
|
||||
});
|
||||
expect(heal.mid, 'ctx trebuie suspendat inainte de self-heal').toBe('suspended');
|
||||
expect(heal.osc, 'beep trebuie sa creeze oscilatoare').toBeGreaterThan(0);
|
||||
expect(heal.end, 'beep trebuie sa reia ctx-ul suspendat').toBe('running');
|
||||
} finally {
|
||||
await gp.close();
|
||||
try { unlinkSync(tmpPath); } catch (_) {}
|
||||
|
||||
Reference in New Issue
Block a user