From 309103fb5983fa5c574969fa327ae1fc918237a6 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sat, 13 Jun 2026 10:45:03 +0000 Subject: [PATCH] =?UTF-8?q?S3=20pas=202:=20hart=C4=83=20overworld=20=C3=AE?= =?UTF-8?q?nlocuie=C8=99te=20coridorul=20=C3=AEn=20campanie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strat de navigare top-down (#overworld) peste #room-frame: jucător care merge pe hartă (săgeți/WASD/dpad) la uși numerotate → intră → camera se montează → revine pe hartă; steag de ieșire deblocat după toate camerele. intro→showOverworld(0), nextRoom/skip/resume→showOverworld. Contractul orchestratorului NESCHIMBAT (mountRoom/nextRoom/roomReady/roomError/timeout 4s/finale/dots/beep). Cod coridor (showCorridor + markup + CSS) șters. Hooks window.__ow pentru teste. Cele 8 teste campanie E2E rescrise pentru noul model (enterRoom/waitOverworld/__ow). Smoke 21/21 (zero regresie) + captură vizuală. Board: TODOS.md S3 pas 2 [x]. Co-Authored-By: Claude Opus 4.8 (1M context) --- TODOS.md | 5 +- escape-builder.html | 209 +++++++++++++++++++++++++++++++++----------- tests/smoke.mjs | 102 ++++++++++----------- 3 files changed, 212 insertions(+), 104 deletions(-) diff --git a/TODOS.md b/TODOS.md index 2ebbe6f..4e0e98a 100644 --- a/TODOS.md +++ b/TODOS.md @@ -45,7 +45,10 @@ portează în `escape-builder.html` (un singur fișier, integrare secvențială) - [x] Pas 1 — Bomberman în `gameArcade` (GATA). Păstrează `openPuzzle`/`onDoorSolved`/`showFinal`/ `modalOpen()`/`roomReady`; uși=N puzzle-uri, cufăr=scăpare. Demo regenerat. Smoke 21/21 + verificare gameplay 6/6 (`scratch/verify-arcade-integrated.mjs`) + captură. - - [ ] Pas 2 — Overworld în `gameCampaign`. + - [x] Pas 2 — Overworld în `gameCampaign` (GATA). Hartă top-down `#overworld` înlocuiește + coridorul; intro→`showOverworld(0)`, nextRoom/skip/resume→`showOverworld`. Contractul + (mountRoom/nextRoom/roomReady/roomError/timeout/finale) NESCHIMBAT. Cod coridor șters. + Cele 8 teste campanie rescrise (`enterRoom`/`waitOverworld`/`__ow`). Smoke 21/21 + captură. - [ ] Pas 3 — restyle 5 stiluri (din `STYLES.md`). - [!] **S4 — extinde `tests/smoke.mjs`** *(blocat de S3)* — bomberman, hartă, audio, regresie. diff --git a/escape-builder.html b/escape-builder.html index ba818d7..dbb7b66 100644 --- a/escape-builder.html +++ b/escape-builder.html @@ -1603,14 +1603,27 @@ body { #intro h1 { margin: 0; font-size: clamp(22px,5vw,36px); font-weight: 900; } #intro .story-text { color: rgba(255,255,255,.8); max-width: 56ch; line-height: 1.6; } #intro .promise { color: rgba(255,255,255,.5); font-size: 14px; } -/* Coridor */ -#corridor { background: var(--c-bg); } -#corr-reward { display: flex; align-items: center; gap: 16px; } -#corr-stars { font-size: 26px; letter-spacing: 3px; color: var(--c-gold); } -#corr-letter { font-size: 56px; font-weight: 900; color: var(--c-gold); line-height: 1; } -#corr-label { color: rgba(255,255,255,.6); font-size: 13px; } -#corr-next { color: rgba(255,255,255,.75); font-size: 15px; font-weight: 600; } -#corr-door { display: flex; align-items: center; justify-content: center; flex: 1; min-height: 0; padding: 8px 0; } +/* ===== Overworld (hartă top-down — înlocuiește coridorul) ===== */ +#overworld.overlay { padding: 0; gap: 0; background: var(--c-bg); } +#ow-wrap { position: relative; flex: 1; width: 100%; overflow: hidden; } +#ow-world { position: absolute; left: 0; top: 0; transition: transform .12s linear; } +.ow-tile { position: absolute; width: 40px; height: 40px; } +.ow-floor { background: #2a1d4d; } +.ow-floor.alt { background: #2f2156; } +.ow-wall { background: #14092e; box-shadow: inset 0 0 0 1px rgba(0,0,0,.35); } +.ow-door { position: absolute; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 15px; color: #fff; border-radius: 7px; background: #e11d48; box-shadow: 0 2px 8px rgba(0,0,0,.5); } +.ow-door.solved { background: var(--c-gold); color: #3a2606; } +.ow-door.target { box-shadow: 0 0 0 3px #a78bfa, 0 2px 10px rgba(167,139,250,.6); } +.ow-exit { position: absolute; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; font-size: 20px; border-radius: 7px; background: #3b2a63; filter: grayscale(1) brightness(.7); } +.ow-exit.open { background: #166534; filter: none; box-shadow: 0 0 14px #22c55e; } +.ow-player { position: absolute; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; font-size: 24px; transition: left .1s linear, top .1s linear; z-index: 3; } +#ow-hint { position: absolute; left: 0; right: 0; bottom: 8px; text-align: center; font-size: 13px; color: rgba(255,255,255,.72); z-index: 4; pointer-events: none; padding: 0 8px; } +#ow-toast { position: absolute; left: 50%; top: 10px; transform: translateX(-50%); background: rgba(0,0,0,.72); padding: 6px 14px; border-radius: 20px; font-size: 14px; font-weight: 700; color: var(--c-gold); z-index: 4; opacity: 0; transition: opacity .3s; pointer-events: none; } +#ow-toast.show { opacity: 1; } +#ow-dpad { position: absolute; right: 10px; bottom: 10px; display: grid; grid-template-columns: repeat(3, 42px); grid-template-rows: repeat(3, 42px); gap: 4px; z-index: 5; } +#ow-dpad button { border: 1px solid #4a3590; background: rgba(34,22,67,.85); color: #cdc3f0; border-radius: 9px; font-size: 16px; cursor: pointer; } +#ow-dpad button:active { background: var(--accent); } +#ow-dpad .sp { visibility: hidden; } /* Skip */ #skip-banner { background: var(--c-bg); } /* ===== UȘILE — 5 stiluri × 3 stări ===== */ @@ -1744,17 +1757,17 @@ body { -
-
-
-
Litera câștigată
-
+
+
+
+
+
+
+ + +
-
-
-
-
@@ -1820,7 +1833,6 @@ function clearProgress(){ try{ sessionStorage.removeItem(_RESUME_KEY); }catch(e) var frameEl = document.getElementById('room-frame'); var introEl = document.getElementById('intro'); -var corridorEl = document.getElementById('corridor'); var skipEl = document.getElementById('skip-banner'); var finaleEl = document.getElementById('finale'); @@ -1884,7 +1896,7 @@ window.nextRoom = function(data){ saveProgress(); console.log('[campaign] camera',idx,'done. stars=',data.stars,'letter=',letter); var next = idx + 1; - if(next >= N){ clearProgress(); showFinale(); } else { showCorridor(idx, data, next); } + if(next >= N){ clearProgress(); showFinale(); } else { showOverworld(next, data); } }; window.roomReady = function(idx){ @@ -1963,34 +1975,6 @@ function mountRoom(idx){ console.log('[campaign] montat camera',idx,'stil',style); } -/* ----- Coridor ----- */ -function showCorridor(doneIdx, data, nextIdx){ - hideAll(); - var s = data.stars || 0; var stars = ''; - for(var i=0;i= N){ showFinale(); } else { showCorridor(idx,{stars:0,letter:''},next); } + if(next >= N){ showFinale(); } else { showOverworld(next); } }; } @@ -2044,10 +2028,129 @@ function confetti(){ } } +var overworldEl = document.getElementById('overworld'); function hideAll(){ - [introEl,corridorEl,skipEl,finaleEl].forEach(function(el){ el.classList.remove('show'); }); + [introEl,overworldEl,skipEl,finaleEl].forEach(function(el){ el.classList.remove('show'); }); } +/* ===== Overworld (S3 pas2 — hartă top-down care înlocuiește coridorul) ===== + * Strat de NAVIGARE peste #room-frame. Nu schimbă contractul: + * mountRoom/nextRoom/roomReady/roomError/skip/resume/finale rămân identice. + * Camera done → showOverworld(next) (în loc de showCorridor). */ +var OW_TILE = 40; +var OW_ROWS = 9; +var OW_COLS = Math.max(11, Math.min(19, N * 2 + 5)); +var OW_MIDR = OW_ROWS >> 1; +var owWorld = document.getElementById('ow-world'); +var owWrap = document.getElementById('ow-wrap'); +var owMap = [], owDoors = [], owExit = { col: OW_COLS - 2, row: OW_MIDR }; +var owPlayer = { col: 1, row: OW_MIDR }, owPlayerEl = null, owTargetIdx = 0, owActive = false; + +function owResetPlayer(){ owPlayer.col = 1; owPlayer.row = OW_MIDR; } + +function owBuild(){ + owMap = []; + for (var r = 0; r < OW_ROWS; r++){ owMap[r] = []; for (var c = 0; c < OW_COLS; c++){ owMap[r][c] = (r === 0 || c === 0 || r === OW_ROWS - 1 || c === OW_COLS - 1) ? 1 : 0; } } + owDoors = []; + for (var i = 0; i < N; i++){ + var col = (N <= 1) ? (OW_COLS >> 1) : (3 + Math.round(i * (OW_COLS - 6) / (N - 1))); + var row = OW_MIDR + ((i % 2 === 0) ? -1 : 1) * ((i % 4 < 2) ? 1 : 2); + if (row < 1) row = 1; if (row > OW_ROWS - 2) row = OW_ROWS - 2; + owDoors.push({ col: col, row: row, idx: i }); + } + owWorld.style.width = (OW_COLS * OW_TILE) + 'px'; + owWorld.style.height = (OW_ROWS * OW_TILE) + 'px'; + var html = ''; + for (var r2 = 0; r2 < OW_ROWS; r2++) for (var c2 = 0; c2 < OW_COLS; c2++){ + var cls = owMap[r2][c2] === 1 ? 'ow-wall' : ('ow-floor' + (((r2 + c2) % 2) ? ' alt' : '')); + html += '
'; + } + owDoors.forEach(function(d){ html += '
' + (d.idx + 1) + '
'; }); + html += '
\\ud83c\\udfc1
'; + html += '
\\ud83e\\uddd1
'; + owWorld.innerHTML = html; + owPlayerEl = document.getElementById('ow-player'); +} + +function owAllDone(){ for (var i = 0; i < N; i++) if (!roomDone[i]) return false; return true; } + +function owRefreshDoors(){ + owDoors.forEach(function(d){ + var el = document.getElementById('ow-door-' + d.idx); if (!el) return; + var done = !!roomDone[d.idx], isSkip = !!skipped[d.idx]; + el.className = 'ow-door' + (done ? ' solved' : '') + (!done && d.idx === owTargetIdx ? ' target' : ''); + if (isSkip) el.textContent = '\\ud83d\\udd12'; + else if (done) el.textContent = (MASTER.puzzles[d.idx].letter || '').trim().toUpperCase() || '\\u2713'; + else el.textContent = (d.idx + 1); + }); + var ex = document.getElementById('ow-exit'); if (ex) ex.className = 'ow-exit' + (owAllDone() ? ' open' : ''); + document.getElementById('ow-hint').textContent = owAllDone() + ? 'Toate camerele rezolvate! Mergi la steag \\ud83c\\udfc1 ca să evadezi.' + : 'Mergi la ușa următoare (săgeți / WASD / butoane).'; +} + +function owCenter(){ + var vpW = owWrap.clientWidth, vpH = owWrap.clientHeight; + var worldW = OW_COLS * OW_TILE, worldH = OW_ROWS * OW_TILE; + var px = owPlayer.col * OW_TILE + OW_TILE / 2, py = owPlayer.row * OW_TILE + OW_TILE / 2; + var tx = worldW <= vpW ? (vpW - worldW) / 2 : Math.max(vpW - worldW, Math.min(0, vpW / 2 - px)); + var ty = worldH <= vpH ? (vpH - worldH) / 2 : Math.max(vpH - worldH, Math.min(0, vpH / 2 - py)); + owWorld.style.transform = 'translate(' + tx + 'px,' + ty + 'px)'; +} + +function owRenderPlayer(){ if (owPlayerEl){ owPlayerEl.style.left = (owPlayer.col * OW_TILE) + 'px'; owPlayerEl.style.top = (owPlayer.row * OW_TILE) + 'px'; } owCenter(); } + +function owWalkable(col, row){ if (col < 0 || row < 0 || col >= OW_COLS || row >= OW_ROWS) return false; return owMap[row][col] !== 1; } + +function owMove(dc, dr){ + if (!owActive) return; + var nc = owPlayer.col + dc, nr = owPlayer.row + dr; + if (!owWalkable(nc, nr)) return; + owPlayer.col = nc; owPlayer.row = nr; owRenderPlayer(); owCheckEnter(); +} + +function owCheckEnter(){ + for (var i = 0; i < owDoors.length; i++){ var d = owDoors[i]; if (owPlayer.col === d.col && owPlayer.row === d.row){ if (!roomDone[d.idx]) owEnterDoor(d.idx); return; } } + if (owPlayer.col === owExit.col && owPlayer.row === owExit.row && owAllDone()){ owActive = false; showFinale(); } +} + +function owEnterDoor(idx){ if (!owActive) return; /* idempotență — a doua intrare ignorată (T4/D4) */ owActive = false; mountRoom(idx); } + +function showOverworld(targetIdx, data){ + hideAll(); + owTargetIdx = targetIdx; + owRefreshDoors(); + owRenderPlayer(); + owActive = true; + overworldEl.classList.add('show'); + if (data){ + var s = data.stars || 0; + var letter = String(data.letter || '').replace(/[^A-Za-z0-9]/g, '').charAt(0).toUpperCase(); + var t = (letter ? ('+' + letter + ' ') : '') + (s ? ('+' + s + ' \\u2605') : ''); + var toast = document.getElementById('ow-toast'); + if (t.trim()){ toast.textContent = t; toast.classList.add('show'); setTimeout(function(){ toast.classList.remove('show'); }, 1600); } + } + setTimeout(owCenter, 0); +} + +document.addEventListener('keydown', function(e){ + if (!owActive) return; + var m = { ArrowUp:[0,-1], ArrowDown:[0,1], ArrowLeft:[-1,0], ArrowRight:[1,0], w:[0,-1], s:[0,1], a:[-1,0], d:[1,0] }[e.key]; + if (!m) return; e.preventDefault(); owMove(m[0], m[1]); +}); +document.querySelectorAll('#ow-dpad button[data-d]').forEach(function(b){ + b.addEventListener('click', function(){ var m = { U:[0,-1], D:[0,1], L:[-1,0], R:[1,0] }[b.getAttribute('data-d')]; if (m) owMove(m[0], m[1]); }); +}); + +/* Hooks pentru teste (conduc harta fără tastatură) */ +window.__ow = { + get state(){ return { player: { col: owPlayer.col, row: owPlayer.row }, target: owTargetIdx, active: owActive, allDone: owAllDone(), doors: owDoors.map(function(d){ return { idx: d.idx, col: d.col, row: d.row, solved: !!roomDone[d.idx] }; }) }; }, + enterDoor: function(i){ var d = owDoors[i]; if (d){ owPlayer.col = d.col; owPlayer.row = d.row; owRenderPlayer(); owCheckEnter(); } }, + enterExit: function(){ owPlayer.col = owExit.col; owPlayer.row = owExit.row; owRenderPlayer(); owCheckEnter(); } +}; + +owBuild(); + /* ----- Intro ----- */ document.getElementById('intro-title').textContent = MASTER.title; document.getElementById('intro-story').textContent = (MASTER.player?'Salut, '+MASTER.player+'! ':'')+MASTER.story; @@ -2056,7 +2159,7 @@ 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){} - clearProgress(); mountRoom(0); + clearProgress(); owResetPlayer(); showOverworld(0); }; buildDots(); @@ -2071,13 +2174,15 @@ buildDots(); collected = saved.collected || []; skipped = saved.skipped || {}; Object.keys(skipped).forEach(function(k){ roomDone[+k] = true; setDot(+k,'done'); }); - /* repornim de la coridorul camerei next */ + /* repornim pe hartă, la ușa camerei next */ var resumeIdx = saved.idx + 1; + /* marchează ușile deja rezolvate pe hartă (resume) */ + for(var di=0; di<=saved.idx; di++){ roomDone[di] = true; setDot(di,'done'); } if(resumeIdx >= N){ /* ultima cameră deja terminată — mergi direct la final */ showFinale(); return; } - showCorridor(saved.idx, {stars:0, letter: (collected[collected.length-1]||'')}, resumeIdx); + owResetPlayer(); showOverworld(resumeIdx); })(); <\/script> diff --git a/tests/smoke.mjs b/tests/smoke.mjs index 0611650..4a4b3d3 100644 --- a/tests/smoke.mjs +++ b/tests/smoke.mjs @@ -526,21 +526,19 @@ test.describe('Campanie E2E @campanie', () => { } /** - * Asteapta coridorul si apasa "Deschide usa". - * Asteapta si ca coridorul sa dispara (mountRoom apelat) inainte de return — - * asta garanteaza ca data-room-ready al camerei precedente a fost sters. + * Harta overworld (înlocuiește coridorul, S3 pas2): după ce o cameră e gata, + * orchestratorul arată harta cu jucătorul. Testele conduc harta via __ow. */ - async function openCorridor(gp) { + async function waitOverworld(gp) { await gp.waitForFunction( - () => document.getElementById('corridor')?.classList.contains('show'), + () => window.__ow && window.__ow.state.active, null, { timeout: 10000 } ); - await gp.locator('#btn-next').click(); - // Asteapta inchiderea coridorului (mountRoom apelat dupa 280ms animatie) - await gp.waitForFunction( - () => !document.getElementById('corridor')?.classList.contains('show'), - null, { timeout: 3000 } - ); + } + /** Intră pe ușa camerei `idx` (echivalent cu mersul jucătorului pe hartă). */ + async function enterRoom(gp, idx) { + await waitOverworld(gp); + await gp.evaluate((i) => window.__ow.enterDoor(i), idx); } // ───────────────────────────────────────────────────────────────────── @@ -566,8 +564,8 @@ test.describe('Campanie E2E @campanie', () => { const styles = ['classic', 'terminal', 'arcade', 'chat', 'point']; for (let i = 0; i < 5; i++) { + await enterRoom(gp, i); await solveRoom(gp, styles[i], 'r' + (i + 1)); - if (i < 4) await openCorridor(gp); } // Finale trebuie sa apara @@ -598,7 +596,7 @@ test.describe('Campanie E2E @campanie', () => { // ───────────────────────────────────────────────────────────────────── // Test 2: Resume — reload mid-campanie revine la coridor // ───────────────────────────────────────────────────────────────────── - test('resume — reload mid-campanie returneaza la coridor (safeStore D3+D11) @campanie', + test('resume — reload mid-campanie returneaza la harta (safeStore D3+D11) @campanie', async ({ page }) => { const errors = trackErrors(page); const cfg = campaignCfg(3, 'classic'); @@ -611,20 +609,22 @@ test.describe('Campanie E2E @campanie', () => { await gp.locator('#btn-start').click(); // Rezolva camera 0 (classic) + await enterRoom(gp, 0); await solveRoom(gp, 'classic', 'r1'); - // Asteapta coridorul — saveProgress() a fost apelat, sesionStorage are progresul - await gp.waitForFunction( - () => document.getElementById('corridor')?.classList.contains('show'), - null, { timeout: 10000 } - ); + // Asteapta harta — saveProgress() a fost apelat, sessionStorage are progresul + await waitOverworld(gp); - // Reload — tryResume() trebuie sa redeschida coridorul, NU intro-ul + // Reload — tryResume() trebuie sa redeschida HARTA, NU intro-ul await gp.reload(); await gp.waitForLoadState('domcontentloaded'); - // Coridorul trebuie sa fie vizibil - await expect(gp.locator('#btn-next')).toBeVisible({ timeout: 5000 }); + // Harta trebuie sa fie activa + await gp.waitForFunction( + () => window.__ow && window.__ow.state.active && + document.getElementById('overworld')?.classList.contains('show'), + null, { timeout: 5000 } + ); // Intro-ul NU trebuie sa fie vizibil const introVisible = await gp.locator('#btn-start').isVisible(); @@ -666,6 +666,7 @@ test.describe('Campanie E2E @campanie', () => { }); await gp.locator('#btn-start').click(); + await enterRoom(gp, 0); // monteaza camera moarta (fara roomReady) → timeout 4s // Timeout 4s → skip-banner apare in max 9s (4s timeout + 5s marja) await gp.waitForFunction( @@ -684,11 +685,9 @@ test.describe('Campanie E2E @campanie', () => { // Camera urmatoare (idx=1) se poate deschide await gp.locator('#btn-skip').click(); - // Coridorul sau direct camera urmatoare (daca N>2 ramane coridor) - // Inlocuim si al doilea template sa se deschida coridorul - // skipRoom → showSkipBanner → btn-skip → idx+1 < N → showCorridor + // skipRoom → showSkipBanner → btn-skip → idx+1 < N → showOverworld(next) await gp.waitForFunction( - () => document.getElementById('corridor')?.classList.contains('show') || + () => (window.__ow && window.__ow.state.active) || document.getElementById('skip-banner')?.classList.contains('show'), null, { timeout: 5000 } ); @@ -717,6 +716,7 @@ test.describe('Campanie E2E @campanie', () => { try { await gp.goto('file://' + tmpPath); await gp.locator('#btn-start').click(); + await enterRoom(gp, 0); // Asteapta roomReady pentru camera 0 (data-room-ready setat) await gp.waitForFunction( @@ -751,7 +751,7 @@ test.describe('Campanie E2E @campanie', () => { // ───────────────────────────────────────────────────────────────────── // Test 5: Dublu-click "Deschide usa" — idempotent (T4 + D4) // ───────────────────────────────────────────────────────────────────── - test('dublu-click "Deschide usa" — idempotent (fara stare corupta) @campanie', + test('dubla-intrare usa pe harta — idempotent (fara stare corupta) @campanie', async ({ page }) => { const errors = trackErrors(page); const cfg = campaignCfg(3, 'classic'); @@ -765,25 +765,14 @@ test.describe('Campanie E2E @campanie', () => { await gp.locator('#btn-start').click(); // Rezolva camera 0 + await enterRoom(gp, 0); await solveRoom(gp, 'classic', 'r1'); - // Asteapta coridorul - await gp.waitForFunction( - () => document.getElementById('corridor')?.classList.contains('show'), - null, { timeout: 10000 } - ); + // Asteapta harta (target = camera 1) + await waitOverworld(gp); - // Primul click (normal) — butonul se dezactiveaza imediat - await gp.locator('#btn-next').click(); - - // Al doilea click fortat (butonul e disabled dupa primul click) - await gp.locator('#btn-next').click({ force: true }); - - // Asteapta inchiderea coridorului + mountRoom(1) - await gp.waitForFunction( - () => !document.getElementById('corridor')?.classList.contains('show'), - null, { timeout: 3000 } - ); + // Dubla intrare pe usa 1 — a doua trebuie ignorata (idempotenta, T4/D4) + await gp.evaluate(() => { window.__ow.enterDoor(1); window.__ow.enterDoor(1); }); // Camera 1 trebuie sa se monteze exact o singura data await gp.waitForFunction( @@ -794,15 +783,13 @@ test.describe('Campanie E2E @campanie', () => { // Rezolva camera 1 si verifica starea finala await solveRoom(gp, 'classic', 'r2'); - // Coridorul pentru camera 2 trebuie sa apara (nu sarit din cauza duplicate mount) - await gp.waitForFunction( - () => document.getElementById('corridor')?.classList.contains('show'), - null, { timeout: 10000 } - ); + // Harta pentru camera 2 trebuie sa apara (nu sarit din cauza duplicate mount) + await waitOverworld(gp); - // "Camera 3" trebuie sa fie mentionata in corr-next (nu Camera 4 sau altceva) - const corrNext = await gp.locator('#corr-next').innerText(); - expect(corrNext).toMatch(/[Uu]ltima|3/); // e ultima camera sau Camera 3 + // Target = camera 3 (idx 2); exact 2 usi rezolvate (0+1, fara dubla-numarare) + const st = await gp.evaluate(() => window.__ow.state); + expect(st.target).toBe(2); + expect(st.doors.filter(d => d.solved).length).toBe(2); } finally { await gp.close(); @@ -835,6 +822,7 @@ test.describe('Campanie E2E @campanie', () => { try { await gp.goto('file://' + tmpPath); await gp.locator('#btn-start').click(); + await enterRoom(gp, 0); // Asteapta roomReady await gp.waitForFunction( @@ -877,8 +865,8 @@ test.describe('Campanie E2E @campanie', () => { await gp.locator('#btn-start').click(); for (let i = 0; i < 8; i++) { + await enterRoom(gp, i); await solveRoom(gp, 'classic', 'r' + (i + 1)); - if (i < 7) await openCorridor(gp); } // Finale trebuie sa apara @@ -936,6 +924,18 @@ test.describe('Campanie E2E @campanie', () => { expect(chromeHeight, 'chromeHeight null — #chrome nu exista').not.toBeNull(); expect(chromeHeight, 'Chrome > 40px la 320px').toBeLessThanOrEqual(40); + // Si pe harta (overworld) — fara overflow orizontal la 320px + await gp.locator('#btn-start').click(); + await gp.waitForFunction( + () => window.__ow && window.__ow.state.active, + null, { timeout: 5000 } + ); + await gp.waitForTimeout(200); + const owOverflow = await gp.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1 + ); + expect(owOverflow, 'Overflow orizontal pe harta la 320x568').toBe(false); + } finally { await gp.close(); try { unlinkSync(tmpPath); } catch (_) {}