From d8cb5155452fc50bf593a03a4d1a100a8e18dbc3 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sat, 13 Jun 2026 20:12:32 +0000 Subject: [PATCH] Muzica ambient T10: arpegiu calm care accelereaza sub 1 min MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opt-in din builder (checkbox music, default off). Orchestrator-only: parintele detine AudioContext (reutilizeaza beep._ctx); camerele nu stiu de muzica. - arpegiu pentatonica minora (oscilatoare sine scurte), tempo ~1.8x pe ultimul minut (legat de _deadline-ul Timer Calm); fara timer -> loop calm fara accelerare - buton 🎵/🔇 in bara chrome (#btn-music) - duck pe voce: voiceSay onstart/onend regleaza gain (vocea are prioritate) - fallback fara AudioContext -> no-op, buton ascuns (zero penalizare) - porneste la start + resume; stop la showFinale + toggle - hook test window.__music; exemplu-campanie.html regenerat (ramane opt-in off) Smoke 30/30 (test nou "muzica ambient": opt-in, start, tempo sub 1 min, duck, toggle). Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 4 +- TODOS.md | 22 +++++++---- escape-builder.html | 90 +++++++++++++++++++++++++++++++++++++++---- exemplu-campanie.html | 88 ++++++++++++++++++++++++++++++++++++++---- tests/AGENTS.md | 10 ++--- tests/smoke.mjs | 52 +++++++++++++++++++++++++ 6 files changed, 238 insertions(+), 28 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 23007a2..b4fa10b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,9 +20,9 @@ sursa de adevăr tehnică pentru agenți. python3 -m http.server 8000 # Teste (Playwright; fără package.json commitat — vezi tests/AGENTS.md): -npx playwright test tests/smoke.mjs # suita completă: 29/29 +npx playwright test tests/smoke.mjs # suita completă: 30/30 npx playwright test tests/smoke.mjs --grep @regresie # regresie: 16 -npx playwright test tests/smoke.mjs --grep @campanie # campanie E2E: 15 +npx playwright test tests/smoke.mjs --grep @campanie # campanie E2E: 16 ``` ## Durable Rules (repo-wide) diff --git a/TODOS.md b/TODOS.md index 014958a..2defb53 100644 --- a/TODOS.md +++ b/TODOS.md @@ -18,7 +18,7 @@ Referință plan complet: `~/.gstack/projects/romfast-escape-builder/ceo-plans/2 - [x] **Audit a11y motoare** — LIVRAT (vezi §dedicată mai jos). Smoke 26/26. **PR2 livrat (2026-06-13):** audio camere `651025b`, voce `da93d84`, unificare `ab11089`, a11y (acest commit). -Rămas din Etapa 2: muzică timer (T10) + Adventure Mode v0. (D7 + Timer Calm LIVRATE — vezi §§ mai jos.) +Rămas din Etapa 2: Adventure Mode v0 (+ Diplomă §Design pct.9). (D7 + Timer Calm + Muzică T10 LIVRATE.) ### [x] Bomberman polish (feedback user 2026-06-13) — LIVRAT Trei probleme raportate + o lipsă, toate în `gameArcade` (`escape-builder.html`): @@ -106,12 +106,20 @@ safe by default. `exemplu-campanie.html` regenerat (rămâne fără timer — op smoke 29/29 (test nou „timer calm": format M:SS, prag auriu, freeze la expirare, jocul continuă, resume păstrează ceasul). Commit: (acest commit). Următorul: muzică T10 (accelerare sub 1 min — depinde de timer). -### Muzică accelerată la timer (PR2 / T10) — depinde de Timer Calm (LIVRAT) -- Audio ambient în campanie: track calm → accelerare progresivă sub 1 minut. -- Ownership: părintele deține AudioContext; camerele nu știu de muzică. -- Fallback: zero pedeapsă dacă AudioContext lipsă (webview restricitve). -- Edge: muzica se oprește la `speechSynthesis.cancel()` dacă vocea e activă simultan. -- Legat de: T10 (PR2), timer countdown în bara chrome (§Design pct. 10). +### [x] Muzică ambient accelerată la timer (PR2 / T10) — LIVRAT (2026-06-13) +Opt-in din builder (checkbox `music`, default off). Orchestrator-only: părintele deține AudioContext +(reutilizează `beep._ctx`, deblocat de gestul global); camerele NU știu de muzică. Arpegiu calm pe +pentatonică minoră (`_mTick`, oscilatoare sine scurte la ~520ms); tempo **accelerează** spre ~1.8× +pe ultimul minut (`musicTempoFactor`, legat de `_deadline`-ul Timer Calm). Buton 🎵/🔇 în bara chrome +(`#btn-music`). Edge-uri tratate: +- **Duck pe voce:** `voiceSay` setează `u.onstart→duckMusic(true)` / `onend|onerror→duckMusic(false)`; + `voiceCancel` și el unduck. Vocea are prioritate (gain muzică × 0.22 cât timp vorbește). +- **Fallback fără AudioContext:** tot în `try/catch` → no-op, buton ascuns (zero penalizare). +- pornește la „Începe aventura" + la resume; se oprește la `showFinale` (+ toggle). +- fără timer → tempo rămâne 1.0 (loop calm, fără accelerare). +Hook test `window.__music` (`tempo()`, `state()`). `exemplu-campanie.html` regenerat (rămâne fără +muzică — opt-in, ca vocea). Verificat: smoke 30/30 (test nou „muzica ambient": opt-in, start, tempo +crește sub 1 min, duck, toggle). Următorul roadmap: Diplomă (§Design pct.9) + Adventure Mode v0. ### [x] Narațiune vocală (SpeechSynthesis, D10) — LIVRAT (PR2) Feature NOU (nu doar edge-cases — voce nu exista deloc). Opt-in din builder (checkbox diff --git a/escape-builder.html b/escape-builder.html index 2c819aa..2036135 100644 --- a/escape-builder.html +++ b/escape-builder.html @@ -135,6 +135,7 @@ +
Pornesti la "Incepe aventura". Sub 1 minut devine auriu. La expirare ingheata pe 0:00 — jocul continua, fara penalizare.
@@ -179,6 +180,7 @@ const defaultState = () => ({ style: 'classic', charName: 'Alex', voice: false, + music: false, timerMin: 0, story: 'O comoara a fost ascunsa, iar singurul drum spre ea trece prin cateva incercari. Rezolva fiecare puzzle ca sa aduni literele cuvantului magic.', finalMessage: 'Felicitari! Ai gasit comoara!', @@ -1634,15 +1636,15 @@ body { #chrome-timer[hidden] { display: none; } #chrome-timer.low { color: var(--c-gold); } #chrome-timer.expired { color: var(--c-gold); opacity: .55; } -#btn-voice { +#btn-voice, #btn-music { width: 34px; height: 34px; min-width: 34px; padding: 0; border: 0; cursor: pointer; border-radius: 8px; background: rgba(255,255,255,.12); color: #fff; font-size: 17px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; } -#btn-voice[hidden] { display: none; } /* id batea specificitatea UA [hidden] */ -#btn-voice:hover { background: rgba(255,255,255,.22); } -#btn-voice[aria-pressed="false"] { opacity: .5; } -#btn-voice:focus-visible { outline: 2px solid #a78bfa; outline-offset: 2px; } +#btn-voice[hidden], #btn-music[hidden] { display: none; } /* id batea specificitatea UA [hidden] */ +#btn-voice:hover, #btn-music:hover { background: rgba(255,255,255,.22); } +#btn-voice[aria-pressed="false"], #btn-music[aria-pressed="false"] { opacity: .5; } +#btn-voice:focus-visible, #btn-music:focus-visible { outline: 2px solid #a78bfa; outline-offset: 2px; } #dots { display: flex; gap: 8px; } #dots span { width: 10px; height: 10px; border-radius: 50%; @@ -1812,6 +1814,7 @@ body { ${esc(cfg.title)}
+
@@ -1931,6 +1934,56 @@ function startTimer(){ if(!_timerInt && !_timerExpired) _timerInt = setInterval(tickTimer, 1000); } +/* ----- Muzică ambient (T10) — opt-in MASTER.music. Orchestrator-only: părintele + deÈ›ine AudioContext (reutilizează beep._ctx, deblocat de gestul global); camerele + nu È™tiu de muzică. Arpegiu calm pe pentatonică minoră; tempo ACCELEREAZÄ‚ sub 1 min + (legat de Timer Calm). Se atenuează (duck) cât timp vocea vorbeÈ™te. Fallback: + fără AudioContext → no-op, butonul rămâne ascuns (zero penalizare). ----- */ +var MUSIC = !!MASTER.music; +var musicOn = MUSIC; +var _mGain = null, _mTimer = null, _mStep = 0, _mDuck = 1; +var _MSCALE = [0, 3, 5, 7, 10]; /* pentatonică minoră (semitonuri) */ +function _mFreq(semi){ return 220 * Math.pow(2, semi / 12); } +function musicTempoFactor(){ + /* 1.0 normal → ~1.8 pe ultimul minut (accelerare progresivă) */ + if(TIMER_SEC <= 0 || !_deadline) return 1; + var rem = (_deadline - Date.now()) / 1000; + if(rem < 0) return 1.8; + if(rem > 60) return 1; + return 1 + (1 - rem / 60) * 0.8; +} +function _mTick(){ + if(!musicOn || !_mGain){ _mTimer = null; return; } + var ctx = beep._ctx; + if(ctx){ + try{ + var oct = (Math.floor(_mStep / _MSCALE.length) % 2) ? 12 : 0; + var semi = _MSCALE[_mStep % _MSCALE.length] + oct; + var o = ctx.createOscillator(), g = ctx.createGain(), t = ctx.currentTime; + o.type = 'sine'; o.frequency.value = _mFreq(semi); + g.gain.setValueAtTime(0.0001, t); + g.gain.linearRampToValueAtTime(0.05 * _mDuck, t + 0.05); + g.gain.exponentialRampToValueAtTime(0.0001, t + 0.55); + o.connect(g); g.connect(_mGain); + o.start(t); o.stop(t + 0.6); + }catch(e){} + } + _mStep++; + _mTimer = setTimeout(_mTick, 520 / musicTempoFactor()); /* mai rapid când tempo creÈ™te */ +} +function startMusic(){ + if(!musicOn) return; + try{ + var ctx = beep._ctx || (beep._ctx = new (window.AudioContext || window.webkitAudioContext)()); + if(ctx.state === 'suspended') ctx.resume(); + if(!_mGain){ _mGain = ctx.createGain(); _mGain.gain.value = 1; _mGain.connect(ctx.destination); } + if(!_mTimer){ _mStep = 0; _mTick(); } + }catch(e){} +} +function stopMusic(){ if(_mTimer){ clearTimeout(_mTimer); _mTimer = null; } } +function duckMusic(on){ _mDuck = on ? 0.22 : 1; } /* vocea are prioritate (edge T10) */ +window.__music = { tempo: musicTempoFactor, state: function(){ return { on: musicOn, playing: !!_mTimer, duck: _mDuck }; } }; + var frameEl = document.getElementById('room-frame'); var introEl = document.getElementById('intro'); var skipEl = document.getElementById('skip-banner'); @@ -2025,7 +2078,7 @@ if(SPEECH){ _pickVoice(); try{ window.speechSynthesis.onvoiceschanged = _pickVoice; }catch(e){} } -function voiceCancel(){ if(SPEECH){ try{ window.speechSynthesis.cancel(); }catch(e){} } } +function voiceCancel(){ if(SPEECH){ try{ window.speechSynthesis.cancel(); }catch(e){} } duckMusic(false); } function voiceSay(text){ if(!SPEECH || !voiceOn || !text) return; try{ @@ -2034,6 +2087,10 @@ function voiceSay(text){ var u = new SpeechSynthesisUtterance(String(text)); if(_roVoice){ u.voice = _roVoice; u.lang = _roVoice.lang; } else { u.lang = 'ro-RO'; } u.rate = 1; u.pitch = 1; + /* vocea are prioritate → atenuează muzica cât timp vorbeÈ™te (edge T10) */ + u.onstart = function(){ duckMusic(true); }; + u.onend = function(){ duckMusic(false); }; + u.onerror = function(){ duckMusic(false); }; window.speechSynthesis.speak(u); }catch(e){} } @@ -2158,7 +2215,7 @@ function showSkipBanner(idx, code, reason){ /* ----- Final ----- */ function showFinale(){ - stopTimer(); /* jocul s-a încheiat — opreÈ™te ceasul */ + stopTimer(); stopMusic(); /* jocul s-a încheiat — opreÈ™te ceasul + muzica */ hideAll(); finaleEl.classList.add('show'); var wEl = document.getElementById('fin-word'); wEl.innerHTML = ''; collected.forEach(function(l,j){ @@ -2328,6 +2385,7 @@ document.getElementById('btn-start').onclick = function(){ unlockAudio(); /* gest direct pe părinte (handlerul global prinde È™i el, dar e idempotent) */ clearProgress(); owResetPlayer(); showOverworld(0); startTimer(); /* ceasul porneÈ™te exact la „Începe aventura" (intro necronometrat) */ + startMusic(); /* muzica ambient porneÈ™te odată cu aventura (T10) */ voiceSay(_introStory); /* după showOverworld → hideAll a rulat deja, nu mai anulează (D10) */ }; @@ -2348,6 +2406,23 @@ if(SPEECH && btnVoice){ }; } +/* ----- Buton muzică în bara chrome (T10) ----- */ +var btnMusic = document.getElementById('btn-music'); +if(MUSIC && btnMusic){ + btnMusic.hidden = false; + var _syncMusicBtn = function(){ + btnMusic.innerHTML = musicOn ? '🎵' : '🔇'; /* 🎵 / 🔇 */ + btnMusic.setAttribute('aria-pressed', musicOn ? 'true' : 'false'); + btnMusic.title = musicOn ? 'Muzica pornita — apasa ca sa opresti' : 'Muzica oprita — apasa ca sa pornesti'; + }; + _syncMusicBtn(); + btnMusic.onclick = function(){ + musicOn = !musicOn; + if(musicOn) startMusic(); else stopMusic(); + _syncMusicBtn(); + }; +} + buildDots(); /* ----- Resume la reload (D11) ----- */ @@ -2370,6 +2445,7 @@ buildDots(); } owResetPlayer(); showOverworld(resumeIdx); startTimer(); /* resume → reia ceasul de la deadline-ul absolut salvat */ + startMusic(); /* resume → reia muzica (T10) */ })(); <\/script> diff --git a/exemplu-campanie.html b/exemplu-campanie.html index 893ce71..3078c01 100644 --- a/exemplu-campanie.html +++ b/exemplu-campanie.html @@ -38,15 +38,15 @@ body { #chrome-timer[hidden] { display: none; } #chrome-timer.low { color: var(--c-gold); } #chrome-timer.expired { color: var(--c-gold); opacity: .55; } -#btn-voice { +#btn-voice, #btn-music { width: 34px; height: 34px; min-width: 34px; padding: 0; border: 0; cursor: pointer; border-radius: 8px; background: rgba(255,255,255,.12); color: #fff; font-size: 17px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; } -#btn-voice[hidden] { display: none; } /* id batea specificitatea UA [hidden] */ -#btn-voice:hover { background: rgba(255,255,255,.22); } -#btn-voice[aria-pressed="false"] { opacity: .5; } -#btn-voice:focus-visible { outline: 2px solid #a78bfa; outline-offset: 2px; } +#btn-voice[hidden], #btn-music[hidden] { display: none; } /* id batea specificitatea UA [hidden] */ +#btn-voice:hover, #btn-music:hover { background: rgba(255,255,255,.22); } +#btn-voice[aria-pressed="false"], #btn-music[aria-pressed="false"] { opacity: .5; } +#btn-voice:focus-visible, #btn-music:focus-visible { outline: 2px solid #a78bfa; outline-offset: 2px; } #dots { display: flex; gap: 8px; } #dots span { width: 10px; height: 10px; border-radius: 50%; @@ -216,6 +216,7 @@ body { Comoara ascunsa
+
@@ -335,6 +336,56 @@ function startTimer(){ if(!_timerInt && !_timerExpired) _timerInt = setInterval(tickTimer, 1000); } +/* ----- Muzică ambient (T10) — opt-in MASTER.music. Orchestrator-only: părintele + deÈ›ine AudioContext (reutilizează beep._ctx, deblocat de gestul global); camerele + nu È™tiu de muzică. Arpegiu calm pe pentatonică minoră; tempo ACCELEREAZÄ‚ sub 1 min + (legat de Timer Calm). Se atenuează (duck) cât timp vocea vorbeÈ™te. Fallback: + fără AudioContext → no-op, butonul rămâne ascuns (zero penalizare). ----- */ +var MUSIC = !!MASTER.music; +var musicOn = MUSIC; +var _mGain = null, _mTimer = null, _mStep = 0, _mDuck = 1; +var _MSCALE = [0, 3, 5, 7, 10]; /* pentatonică minoră (semitonuri) */ +function _mFreq(semi){ return 220 * Math.pow(2, semi / 12); } +function musicTempoFactor(){ + /* 1.0 normal → ~1.8 pe ultimul minut (accelerare progresivă) */ + if(TIMER_SEC <= 0 || !_deadline) return 1; + var rem = (_deadline - Date.now()) / 1000; + if(rem < 0) return 1.8; + if(rem > 60) return 1; + return 1 + (1 - rem / 60) * 0.8; +} +function _mTick(){ + if(!musicOn || !_mGain){ _mTimer = null; return; } + var ctx = beep._ctx; + if(ctx){ + try{ + var oct = (Math.floor(_mStep / _MSCALE.length) % 2) ? 12 : 0; + var semi = _MSCALE[_mStep % _MSCALE.length] + oct; + var o = ctx.createOscillator(), g = ctx.createGain(), t = ctx.currentTime; + o.type = 'sine'; o.frequency.value = _mFreq(semi); + g.gain.setValueAtTime(0.0001, t); + g.gain.linearRampToValueAtTime(0.05 * _mDuck, t + 0.05); + g.gain.exponentialRampToValueAtTime(0.0001, t + 0.55); + o.connect(g); g.connect(_mGain); + o.start(t); o.stop(t + 0.6); + }catch(e){} + } + _mStep++; + _mTimer = setTimeout(_mTick, 520 / musicTempoFactor()); /* mai rapid când tempo creÈ™te */ +} +function startMusic(){ + if(!musicOn) return; + try{ + var ctx = beep._ctx || (beep._ctx = new (window.AudioContext || window.webkitAudioContext)()); + if(ctx.state === 'suspended') ctx.resume(); + if(!_mGain){ _mGain = ctx.createGain(); _mGain.gain.value = 1; _mGain.connect(ctx.destination); } + if(!_mTimer){ _mStep = 0; _mTick(); } + }catch(e){} +} +function stopMusic(){ if(_mTimer){ clearTimeout(_mTimer); _mTimer = null; } } +function duckMusic(on){ _mDuck = on ? 0.22 : 1; } /* vocea are prioritate (edge T10) */ +window.__music = { tempo: musicTempoFactor, state: function(){ return { on: musicOn, playing: !!_mTimer, duck: _mDuck }; } }; + var frameEl = document.getElementById('room-frame'); var introEl = document.getElementById('intro'); var skipEl = document.getElementById('skip-banner'); @@ -429,7 +480,7 @@ if(SPEECH){ _pickVoice(); try{ window.speechSynthesis.onvoiceschanged = _pickVoice; }catch(e){} } -function voiceCancel(){ if(SPEECH){ try{ window.speechSynthesis.cancel(); }catch(e){} } } +function voiceCancel(){ if(SPEECH){ try{ window.speechSynthesis.cancel(); }catch(e){} } duckMusic(false); } function voiceSay(text){ if(!SPEECH || !voiceOn || !text) return; try{ @@ -438,6 +489,10 @@ function voiceSay(text){ var u = new SpeechSynthesisUtterance(String(text)); if(_roVoice){ u.voice = _roVoice; u.lang = _roVoice.lang; } else { u.lang = 'ro-RO'; } u.rate = 1; u.pitch = 1; + /* vocea are prioritate → atenuează muzica cât timp vorbeÈ™te (edge T10) */ + u.onstart = function(){ duckMusic(true); }; + u.onend = function(){ duckMusic(false); }; + u.onerror = function(){ duckMusic(false); }; window.speechSynthesis.speak(u); }catch(e){} } @@ -562,7 +617,7 @@ function showSkipBanner(idx, code, reason){ /* ----- Final ----- */ function showFinale(){ - stopTimer(); /* jocul s-a încheiat — opreÈ™te ceasul */ + stopTimer(); stopMusic(); /* jocul s-a încheiat — opreÈ™te ceasul + muzica */ hideAll(); finaleEl.classList.add('show'); var wEl = document.getElementById('fin-word'); wEl.innerHTML = ''; collected.forEach(function(l,j){ @@ -732,6 +787,7 @@ document.getElementById('btn-start').onclick = function(){ unlockAudio(); /* gest direct pe părinte (handlerul global prinde È™i el, dar e idempotent) */ clearProgress(); owResetPlayer(); showOverworld(0); startTimer(); /* ceasul porneÈ™te exact la „Începe aventura" (intro necronometrat) */ + startMusic(); /* muzica ambient porneÈ™te odată cu aventura (T10) */ voiceSay(_introStory); /* după showOverworld → hideAll a rulat deja, nu mai anulează (D10) */ }; @@ -752,6 +808,23 @@ if(SPEECH && btnVoice){ }; } +/* ----- Buton muzică în bara chrome (T10) ----- */ +var btnMusic = document.getElementById('btn-music'); +if(MUSIC && btnMusic){ + btnMusic.hidden = false; + var _syncMusicBtn = function(){ + btnMusic.innerHTML = musicOn ? '🎵' : '🔇'; /* 🎵 / 🔇 */ + btnMusic.setAttribute('aria-pressed', musicOn ? 'true' : 'false'); + btnMusic.title = musicOn ? 'Muzica pornita — apasa ca sa opresti' : 'Muzica oprita — apasa ca sa pornesti'; + }; + _syncMusicBtn(); + btnMusic.onclick = function(){ + musicOn = !musicOn; + if(musicOn) startMusic(); else stopMusic(); + _syncMusicBtn(); + }; +} + buildDots(); /* ----- Resume la reload (D11) ----- */ @@ -774,6 +847,7 @@ buildDots(); } owResetPlayer(); showOverworld(resumeIdx); startTimer(); /* resume → reia ceasul de la deadline-ul absolut salvat */ + startMusic(); /* resume → reia muzica (T10) */ })(); diff --git a/tests/AGENTS.md b/tests/AGENTS.md index 168dbf8..08b769a 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -5,7 +5,7 @@ Smoke + regresie + campanie E2E pentru jocurile generate. Verifică faptic: fiec până la ecranul final, fără erori de consolă. ## Ownership -- `tests/smoke.mjs` — unicul fiÈ™ier de teste (~29 teste). +- `tests/smoke.mjs` — unicul fiÈ™ier de teste (~30 teste). - `playwright.config.mjs` (la root, **gitignored**) — config dev. ## Local Contracts @@ -18,9 +18,9 @@ până la ecranul final, fără erori de consolă. fiecare test asertează `errors.length === 0` la final. - **Tag-uri:** `@regresie` (16 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML + stil top-level invalid la import + bomberman gameplay + bomberman rază/powerup-uri) È™i `@campanie` - (15 — intro→hartă→camere→final, resume, cameră moartă, idempotență ușă, `$`/`$&`, beep, mobil, - audio S1, voce/naraÈ›iune D10, a11y tap/aria/reduced-motion, navigare overworld, timer calm T10). -- **Status È›intă: 29/29 PASS.** + (16 — intro→hartă→camere→final, resume, cameră moartă, idempotență ușă, `$`/`$&`, beep, mobil, + audio S1, voce/naraÈ›iune D10, a11y tap/aria/reduced-motion, navigare overworld, timer calm T10, muzica ambient T10). +- **Status È›intă: 30/30 PASS.** ## Work Guidance - După modificări la motoare (`escape-builder.html`): rulează suita completă; extinde `@regresie` dacă @@ -29,7 +29,7 @@ până la ecranul final, fără erori de consolă. ## Verification ```bash -npx playwright test tests/smoke.mjs # 29/29 +npx playwright test tests/smoke.mjs # 30/30 npx playwright test tests/smoke.mjs --grep @regresie npx playwright test tests/smoke.mjs --grep @campanie ``` diff --git a/tests/smoke.mjs b/tests/smoke.mjs index 75dd472..d3fec1f 100644 --- a/tests/smoke.mjs +++ b/tests/smoke.mjs @@ -739,6 +739,58 @@ test.describe('Campanie E2E @campanie', () => { expect(errors, errors.join('\n')).toHaveLength(0); }); + test('muzica ambient — opt-in, porneste la start, tempo accelereaza sub 1 min, duck pe voce, toggle (T10)', + async ({ page }) => { + const errors = trackErrors(page); + const cfg = campaignCfg(3, 'classic'); + cfg.music = true; + cfg.timerMin = 1; /* timer pornit → tempo poate accelera */ + const tmpPath = await writeCampaignHtml(page, cfg, 'music'); + const gp = await page.context().newPage(); + + try { + await gp.goto('file://' + tmpPath); + + // Butonul de muzica vizibil (opt-in activ); muzica inca neporita + await expect(gp.locator('#btn-music')).toBeVisible(); + expect(await gp.evaluate(() => window.__music.state().playing), 'inca neporita pe intro').toBe(false); + + await gp.locator('#btn-start').click(); + + // Dupa start: muzica ruleaza, buton apasat + await gp.waitForFunction(() => window.__music.state().playing === true, null, { timeout: 4000 }); + await expect(gp.locator('#btn-music')).toHaveAttribute('aria-pressed', 'true'); + + // Tempo: 1.0 cand >60s ramase; creste progresiv sub 1 min (citit determinist) + const tempos = await gp.evaluate(() => { + const f = window.__music.tempo; + _deadline = Date.now() + 90000; const t90 = f(); + _deadline = Date.now() + 30000; const t30 = f(); + _deadline = Date.now() + 1000; const t1 = f(); + return { t90, t30, t1 }; + }); + expect(tempos.t90).toBeCloseTo(1, 1); + expect(tempos.t30, 'accelereaza sub 1 min').toBeGreaterThan(tempos.t90); + expect(tempos.t1, 'mai rapid spre expirare').toBeGreaterThan(tempos.t30); + + // Duck: vocea are prioritate → atenueaza muzica + const ducked = await gp.evaluate(() => { duckMusic(true); return window.__music.state().duck; }); + const unducked = await gp.evaluate(() => { duckMusic(false); return window.__music.state().duck; }); + expect(ducked, 'duck activ < 1').toBeLessThan(1); + expect(unducked, 'duck dezactivat = 1').toBe(1); + + // Toggle off din buton → se opreste + await gp.locator('#btn-music').click(); + await expect(gp.locator('#btn-music')).toHaveAttribute('aria-pressed', 'false'); + expect(await gp.evaluate(() => window.__music.state().playing), 'oprit dupa toggle').toBe(false); + + } finally { + await gp.close(); + try { unlinkSync(tmpPath); } catch (_) {} + } + expect(errors, errors.join('\n')).toHaveLength(0); + }); + // ───────────────────────────────────────────────────────────────────── // Test 3: Camera moartă — timeout 4s → skip-banner + cod eroare // ─────────────────────────────────────────────────────────────────────