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>