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

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