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:
5
TODOS.md
5
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.
|
||||
|
||||
|
||||
@@ -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">▲</button><button class="sp"></button>
|
||||
<button data-d="L">◀</button><button class="sp"></button><button data-d="R">▶</button>
|
||||
<button class="sp"></button><button data-d="D">▼</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>
|
||||
|
||||
102
tests/smoke.mjs
102
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 (_) {}
|
||||
|
||||
Reference in New Issue
Block a user