S3 pas 2: hartă overworld înlocuiește coridorul în campanie

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) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-13 10:45:03 +00:00
parent d67f6ddc15
commit 309103fb59
3 changed files with 212 additions and 104 deletions

View File

@@ -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.

View File

@@ -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 {
<button class="btn-main" id="btn-start">Începe aventura</button>
</div>
<div id="corridor" class="overlay">
<div id="corr-reward">
<div>
<div id="corr-label">Litera câștigată</div>
<div id="corr-letter"></div>
<div id="overworld" class="overlay">
<div id="ow-wrap">
<div id="ow-world"></div>
<div id="ow-toast"></div>
<div id="ow-hint"></div>
<div id="ow-dpad">
<button class="sp"></button><button data-d="U">&#9650;</button><button class="sp"></button>
<button data-d="L">&#9664;</button><button class="sp"></button><button data-d="R">&#9654;</button>
<button class="sp"></button><button data-d="D">&#9660;</button><button class="sp"></button>
</div>
<div id="corr-stars"></div>
</div>
<div id="corr-door"></div>
<div id="corr-next"></div>
<button class="btn-main" id="btn-next">Deschide ușa →</button>
</div>
<div id="skip-banner" class="overlay">
@@ -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<s;i++) stars += '\\u2605';
document.getElementById('corr-stars').textContent = stars || '\\u2606';
var letter = String(data.letter||'').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
document.getElementById('corr-letter').textContent = letter || '\\u2014';
var styleNames = {classic:'Clasic',terminal:'Terminal Retro',arcade:'Arcade Pixel',chat:'Story Chat',point:'Point-and-Click'};
var nextStyle = (MASTER.puzzles[nextIdx] && (MASTER.puzzles[nextIdx].style || ROTATION[nextIdx%5])) || 'classic';
var isLast = (nextIdx === N - 1);
document.getElementById('corr-next').textContent =
isLast ? '\\u2605 Ultima cameră!' : 'Camera '+(nextIdx+1)+' — '+styleNames[nextStyle];
/* Ușa ca erou (§Design pct.2 + pct.6) */
var doorEl = document.getElementById('corr-door');
doorEl.innerHTML = doorHtml(nextStyle, isLast, false);
corridorEl.classList.add('show');
var btn = document.getElementById('btn-next');
btn.disabled = false;
btn.onclick = function(){
btn.disabled = true; /* idempotență buton (T4) */
/* Animație deschidere ușă ~250ms (§Design pct.4) */
var d = doorEl.firstElementChild;
if(d) d.classList.add('opening');
setTimeout(function(){ mountRoom(nextIdx); }, 280);
};
}
/* ----- Skip banner ----- */
function showSkipBanner(idx, code, reason){
hideAll();
@@ -2003,7 +1987,7 @@ function showSkipBanner(idx, code, reason){
btn.disabled = false;
btn.onclick = function(){
btn.disabled = true;
if(next >= 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 += '<div class="ow-tile ' + cls + '" style="left:' + (c2 * OW_TILE) + 'px;top:' + (r2 * OW_TILE) + 'px"></div>';
}
owDoors.forEach(function(d){ html += '<div class="ow-door" id="ow-door-' + d.idx + '" style="left:' + (d.col * OW_TILE) + 'px;top:' + (d.row * OW_TILE) + 'px">' + (d.idx + 1) + '</div>'; });
html += '<div class="ow-exit" id="ow-exit" style="left:' + (owExit.col * OW_TILE) + 'px;top:' + (owExit.row * OW_TILE) + 'px">\\ud83c\\udfc1</div>';
html += '<div class="ow-player" id="ow-player" style="left:' + (owPlayer.col * OW_TILE) + 'px;top:' + (owPlayer.row * OW_TILE) + 'px">\\ud83e\\uddd1</div>';
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>
</body>

View File

@@ -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 (_) {}