From ab11089097d5a672f474675bf0bc252cab9fc4dc Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sat, 13 Jun 2026 11:53:37 +0000 Subject: [PATCH] PR2: unifica contractul _campaign la final in libJS.campaignDone() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Payload-ul parent.nextRoom({idx,stars,letter}) era scris identic in 3 locuri (terminal finale(), SNIP.finalJs showFinal(), classic next()). Acum traieste o singura data in libJS.campaignDone(), langa roomReady/beep/onerror. Decizie: NU am pus terminalul pe showFinal() din SNIP (cum sugera formularea initiala) — showFinal randeaza modal #fOverlay, dar terminalul are finale CRT stilizat (ASCII 'EVADARE REUSITA' + RESTART), on-theme intentionat. Fortarea modalului ar fi regresie vizuala pe terminalul standalone. Am unificat doar ramura _campaign (payload identic), prezentarea standalone neatinsa. - terminal finale(): say([...], 'ok', campaignDone) - SNIP.finalJs showFinal(): if(_campaign){ campaignDone(); return; } - arcade/chat/point primesc campaignDone via showFinal automat. - classic ramane bespoke (nu foloseste libJS) — pliere = D7 (documentat in TODOS). Suita 25/25 (terminal standalone + camere terminal in campanie E2E). Demo-uri libJS regenerate; exemplu-clasic.html neatins. Co-Authored-By: Claude Opus 4.8 (1M context) --- TODOS.md | 28 ++++++++++++++++++++++------ escape-builder.html | 13 +++++-------- exemplu-arcade.html | 9 ++++----- exemplu-campanie.html | 2 +- exemplu-chat.html | 9 ++++----- exemplu-point.html | 9 ++++----- exemplu-terminal.html | 7 ++++--- 7 files changed, 44 insertions(+), 33 deletions(-) diff --git a/TODOS.md b/TODOS.md index d583f05..2ffe740 100644 --- a/TODOS.md +++ b/TODOS.md @@ -14,7 +14,7 @@ Referință plan complet: `~/.gstack/projects/romfast-escape-builder/ceo-plans/2 - [x] **Audio camere** — fix REAL (vezi S1 mai jos, commit `651025b`): unlock pe primul gest global (acoperă resume), nu doar btn-start; test rescris (headless crea ctx `running` trivial). - [x] **Narațiune vocală (D10)** — LIVRAT (vezi §„Narațiune vocală" mai jos). Smoke 25/25. -- [ ] **Unificare `finale()` terminal pe `SNIP.finalJs`** (vezi §dedicată mai jos). +- [x] **Unificare contract `_campaign` la final** — `libJS.campaignDone()` (vezi §dedicată mai jos). - [ ] **Audit a11y motoare** (vezi §dedicată mai jos). --- @@ -101,11 +101,27 @@ Verificat: smoke 25/25 (test nou „voce — naratiune opt-in") + live MCP (buto NOTĂ scope: motoarele NU cheamă încă `parent.voiceSay` (am evitat dublu-citit cu roomReady); dacă pe viitor vrei replici chat citite individual, adaugă în `charMsg` cu guard `typeof parent.voiceSay`. -### Unificarea `finale()` din terminal pe `SNIP.finalJs` (PR2 primul pas) -- Astăzi terminalul are propria funcție `finale()` (escape-builder.html:863) care NU folosește `SNIP.finalJs`. -- Migrarea pe SNIP.finalJs deblochează ramura `_campaign` uniformă pentru toate cele 5 motoare. -- Prim pas al Etapei 2 (D7): migrarea `gameClassic` pe `libJS+SNIP` → regresie manuală pe classic. -- Referință: planul §Etapa 2 pct. 1; D7. +### [x] Unificarea contractului `_campaign` la final — `libJS.campaignDone()` (LIVRAT) +**Decizie de design (abatere de la formularea inițială):** NU am pus terminalul pe `showFinal()` +din `SNIP.finalJs`. Motiv: `showFinal()` randează un modal mov `#fOverlay`, iar terminalul are +finale stilizat în CRT (ASCII „EVADARE REUSITA" + comandă `RESTART`) — e on-theme intenționat; +forțarea modalului ar fi o **regresie vizuală** pe terminalul standalone. +Ce am unificat în schimb (adevărata duplicare): payload-ul `parent.nextRoom({idx,stars,letter})` +era scris identic în 3 locuri (terminal `finale()`, `SNIP.finalJs showFinal()`, classic `next()`). +Acum trăiește o singură dată în `libJS.campaignDone()` (lângă `roomReady`/`beep`/`onerror`). +- terminal `finale()` ramura `_campaign` → `say([... CAMERA REZOLVATA ...], 'ok', campaignDone)`. +- `SNIP.finalJs showFinal()` ramura `_campaign` → `campaignDone()`. +- arcade/chat/point folosesc `showFinal` → primesc automat `campaignDone`. +- **classic rămâne bespoke** (nu folosește `libJS`) → contractul lui e încă inline. Pliere completă + = D7 (migrarea `gameClassic` pe `libJS+SNIP`, cu regresie manuală pe classic). RĂMAs DE FĂCUT. +Verificat: smoke 25/25 (terminal standalone test 2 + camere terminal în campanie E2E test 1). +Referință: planul §Etapa 2 pct. 1; D7. + +### [ ] D7 rămas: migrarea `gameClassic` pe `libJS+SNIP` +- Classic (escape-builder.html:451) e singurul motor bespoke: propriul `totalStars`, `beep`, + inline `finalWord` (dublat de 2 ori în `next()`), propriul modal final `#sFinal`. +- După migrare: classic folosește `libJS.campaignDone()` + `SNIP` ca celelalte 4 → 5/5 uniform. +- Necesită regresie manuală pe classic standalone (e demo-ul implicit, cel mai vizibil). ### Audit a11y motoare existente (post-PR1, sub harness Playwright) - **Ținte tap ≥ 44px**: dpad arcade, butoane tf/choice, butonul "Trimite" din chat. diff --git a/escape-builder.html b/escape-builder.html index 65f54f7..728ff8a 100644 --- a/escape-builder.html +++ b/escape-builder.html @@ -755,6 +755,9 @@ function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type function beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} } function confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i < 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } } function roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } } +/* Contract de finalizare a camerei — un singur loc pentru payload-ul parent.nextRoom + (înlocuiește duplicatele din showFinal/finale; D7). Citește totalStars + finalWord() la apel. */ +function campaignDone(){ if(CFG._campaign){ try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:finalWord().charAt(0)}); }catch(e){} } } window.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } }; if(CFG._campaign){ /* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */ @@ -859,11 +862,7 @@ SNIP.finalHtml = `
`; SNIP.finalJs = `function showFinal(){ - if(CFG._campaign){ - var L = finalWord().charAt(0); - try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L}); }catch(e){} - return; - } + if(CFG._campaign){ campaignDone(); return; } el('fStars').textContent = totalStars + ' / ' + (CFG.puzzles.length * 3) + ' \\u2605'; var w = finalWord(); var bw = el('fWord'); bw.innerHTML = ''; for (var j = 0; j < w.length; j++){ var s = document.createElement('span'); s.textContent = w.charAt(j); s.style.animationDelay = (j * 0.18) + 's'; bw.appendChild(s); } @@ -958,9 +957,7 @@ function finale(){ done = true; if(CFG._campaign){ var s = totalStars; var L = finalWord().charAt(0); - say(['>> CAMERA REZOLVATA! Stele: ' + s + (L ? ' | Litera: ' + L : '')], 'ok', function(){ - try{ parent.nextRoom({idx:CFG._campaign.idx, stars:s, letter:L}); }catch(e){} - }); + say(['>> CAMERA REZOLVATA! Stele: ' + s + (L ? ' | Litera: ' + L : '')], 'ok', campaignDone); return; } var w = finalWord().split('').join(' '); diff --git a/exemplu-arcade.html b/exemplu-arcade.html index af6efbe..8ab7f06 100644 --- a/exemplu-arcade.html +++ b/exemplu-arcade.html @@ -90,6 +90,9 @@ function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type function beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} } function confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i < 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } } function roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } } +/* Contract de finalizare a camerei — un singur loc pentru payload-ul parent.nextRoom + (înlocuiește duplicatele din showFinal/finale; D7). Citește totalStars + finalWord() la apel. */ +function campaignDone(){ if(CFG._campaign){ try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:finalWord().charAt(0)}); }catch(e){} } } window.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } }; if(CFG._campaign){ /* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */ @@ -359,11 +362,7 @@ function mCheck(given){ } } function showFinal(){ - if(CFG._campaign){ - var L = finalWord().charAt(0); - try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L}); }catch(e){} - return; - } + if(CFG._campaign){ campaignDone(); return; } el('fStars').textContent = totalStars + ' / ' + (CFG.puzzles.length * 3) + ' \u2605'; var w = finalWord(); var bw = el('fWord'); bw.innerHTML = ''; for (var j = 0; j < w.length; j++){ var s = document.createElement('span'); s.textContent = w.charAt(j); s.style.animationDelay = (j * 0.18) + 's'; bw.appendChild(s); } diff --git a/exemplu-campanie.html b/exemplu-campanie.html index a624fe7..3d2f4ad 100644 --- a/exemplu-campanie.html +++ b/exemplu-campanie.html @@ -258,7 +258,7 @@ body { * roomDone[idx]=true după primul nextRoom → duplicatele ignorate. * Timeout 4s → skipRoom → aceeași compoziție de coridor (D5). */ -var TPL = {"classic":"\u003c!doctype html>\n\u003chtml lang=\"ro\">\n\u003chead>\n\u003cmeta charset=\"utf-8\">\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\u003ctitle>\u003c/title>\n\u003cstyle>\n * { box-sizing: border-box; }\n body {\n margin: 0; min-height: 100vh; font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n color: #f1f0ff; display: flex; align-items: center; justify-content: center; padding: 16px;\n background: radial-gradient(ellipse at 50% 30%, #2a0e5e 0%, #0e0622 70%);\n }\n .card {\n width: 100%; max-width: 560px; background: #1a0e3d;\n border: 1px solid rgba(255,255,255,.18); border-radius: 20px; padding: 26px;\n backdrop-filter: blur(6px);\n box-shadow: 0 0 0 1px rgba(255,255,255,.06), 0 24px 60px rgba(0,0,0,.55), 0 0 40px rgba(109,40,217,.35);\n }\n h1 { margin: 0 0 6px; font-size: 26px; text-align: center; }\n .story { color: rgba(255,255,255,.8); text-align: center; line-height: 1.5; }\n .screen { display: none; }\n .screen.on { display: block; animation: pop .35s cubic-bezier(.22,1,.36,1); }\n @keyframes pop { from { transform: scale(.94) translateY(6px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }\n .progress { height: 10px; background: rgba(255,255,255,.12); border-radius: 99px; overflow: hidden; margin: 14px 0 4px; }\n .progress i { display: block; height: 100%; background: var(--accent); width: 0; box-shadow: 0 0 8px var(--accent); transition: width .5s cubic-bezier(.22,1,.36,1); }\n .meta { display: flex; justify-content: space-between; font-size: 12px; color: rgba(255,255,255,.6); margin-bottom: 14px; }\n .letters { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; margin: 14px 0; }\n .tile {\n width: 44px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center;\n font-weight: 800; font-size: 20px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.18);\n color: rgba(255,255,255,.35);\n }\n .tile.won { background: var(--accent); color: #fff; border-color: transparent; box-shadow: 0 0 12px var(--accent); animation: flip .5s cubic-bezier(.34,1.56,.64,1); }\n @keyframes flip { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }\n .qtitle { font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--accent-light); font-weight: 700; }\n .question { font-size: 21px; line-height: 1.5; margin: 8px 0 18px; color: #f1f0ff; }\n input[type=text] {\n width: 100%; font: inherit; font-size: 18px; padding: 11px 13px; border-radius: 10px;\n border: 1px solid rgba(255,255,255,.25); background: rgba(0,0,0,.25); color: #fff; text-align: center;\n }\n input:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-color: transparent; }\n button {\n font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px;\n font-weight: 700; background: var(--accent); color: #fff; width: 100%; margin-top: 10px; min-height: 44px;\n }\n button:hover { filter: brightness(1.12); }\n button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n button.opt { background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.16); font-weight: 600; text-align: left; min-height: 48px; transition: background .15s, border-color .15s; }\n button.opt:hover { background: rgba(255,255,255,.16); border-color: var(--accent); }\n button.hint { background: none; border: none; color: rgba(255,255,255,.55); font-weight: 600; font-size: 13px; width: auto; display: block; margin: 12px auto 0; }\n button.hint:hover { color: #fff; }\n .hinttext { background: rgba(255,255,255,.1); border-radius: 9px; padding: 10px 12px; font-size: 14px; margin-top: 10px; white-space: pre-line; display: none; }\n .feedback { min-height: 22px; text-align: center; font-weight: 700; margin-top: 10px; }\n .feedback.bad { color: #fda4af; }\n .feedback.good { color: #86efac; }\n .shake { animation: shake .4s ease; }\n @keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }\n .stars { text-align: center; font-size: 26px; letter-spacing: 4px; color: #fbbf24; margin: 6px 0; }\n .bigword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 18px 0; }\n .bigword span {\n width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex;\n align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flip .6s ease backwards;\n }\n .confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 5; animation: fall linear forwards; }\n @keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }\n @media (prefers-reduced-motion: reduce) {\n .screen.on, .tile.won, .bigword span, .shake { animation: none; }\n .progress i { transition: none; }\n }\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003cdiv id=\"sStart\" class=\"screen on\">\n \u003ch1 id=\"gtitle\">\u003c/h1>\n \u003cp class=\"story\" id=\"gstory\">\u003c/p>\n \u003cbutton id=\"btnStart\">Incepe aventura\u003c/button>\n \u003c/div>\n\n \u003cdiv id=\"sGame\" class=\"screen\">\n \u003cdiv class=\"progress\">\u003ci id=\"bar\">\u003c/i>\u003c/div>\n \u003cdiv class=\"meta\">\u003cspan id=\"step\">\u003c/span>\u003cspan id=\"score\">\u003c/span>\u003c/div>\n \u003cdiv class=\"letters\" id=\"lettersBar\">\u003c/div>\n \u003cdiv id=\"qbox\">\n \u003cdiv class=\"qtitle\" id=\"qtitle\">\u003c/div>\n \u003cdiv class=\"question\" id=\"qtext\">\u003c/div>\n \u003cdiv id=\"answers\">\u003c/div>\n \u003cdiv class=\"feedback\" id=\"feedback\">\u003c/div>\n \u003cbutton class=\"hint\" id=\"btnHint\">Vreau un indiciu\u003c/button>\n \u003cdiv class=\"hinttext\" id=\"hinttext\">\u003c/div>\n \u003c/div>\n \u003c/div>\n\n \u003cdiv id=\"sFinal\" class=\"screen\">\n \u003ch1>Evadare reusita!\u003c/h1>\n \u003cdiv class=\"stars\" id=\"finalStars\">\u003c/div>\n \u003cdiv class=\"bigword\" id=\"bigword\">\u003c/div>\n \u003cp class=\"story\" id=\"finalMsg\">\u003c/p>\n \u003cbutton id=\"btnAgain\">Joaca din nou\u003c/button>\n \u003c/div>\n\u003c/div>\n\n\u003cscript>\nvar CFG = __CFG__;\ndocument.documentElement.style.setProperty('--accent', CFG.color || '#6d28d9');\ndocument.documentElement.style.setProperty('--accent-light', 'color-mix(in srgb, ' + (CFG.color || '#6d28d9') + ' 40%, white)');\n\nvar idx = 0, totalStars = 0, attempts = 0, hintUsed = false, won = [];\n\nfunction el(id) { return document.getElementById(id); }\nfunction norm(s) {\n return String(s).trim().toLowerCase().normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '').replace(/\\s+/g, ' ').replace(/,/g, '.');\n}\nfunction show(id) {\n var scr = document.querySelectorAll('.screen');\n for (var i = 0; i \u003c scr.length; i++) scr[i].classList.remove('on');\n el(id).classList.add('on');\n}\n\nel('gtitle').textContent = CFG.title;\nvar hello = CFG.player ? 'Salut, ' + CFG.player + '! ' : '';\nel('gstory').textContent = hello + CFG.story;\n\nel('btnStart').onclick = function () { show('sGame'); renderPuzzle(); };\nel('btnAgain').onclick = function () { location.reload(); };\n\nfunction lettersBar() {\n var bar = el('lettersBar');\n bar.innerHTML = '';\n var any = false;\n for (var i = 0; i \u003c CFG.puzzles.length; i++) {\n var L = (CFG.puzzles[i].letter || '').trim();\n if (!L) continue;\n any = true;\n var d = document.createElement('div');\n d.className = 'tile' + (won[i] ? ' won' : '');\n d.textContent = won[i] ? L.toUpperCase() : '?';\n bar.appendChild(d);\n }\n bar.style.display = any ? '' : 'none';\n}\n\nfunction renderPuzzle() {\n var p = CFG.puzzles[idx];\n attempts = 0; hintUsed = false;\n el('bar').style.width = (idx / CFG.puzzles.length * 100) + '%';\n el('step').textContent = 'Puzzle ' + (idx + 1) + ' din ' + CFG.puzzles.length;\n el('score').textContent = totalStars + ' \\u2605';\n el('qtitle').textContent = p.title || 'Puzzle ' + (idx + 1);\n el('qtext').textContent = p.question;\n el('feedback').textContent = ''; el('feedback').className = 'feedback';\n el('hinttext').style.display = 'none';\n el('hinttext').textContent = p.hint || '';\n el('btnHint').style.display = p.hint ? '' : 'none';\n lettersBar();\n\n var box = el('answers');\n box.innerHTML = '';\n if (p.type === 'free') {\n var inp = document.createElement('input');\n inp.type = 'text'; inp.autocomplete = 'off'; inp.placeholder = 'Scrie raspunsul...';\n var btn = document.createElement('button');\n btn.textContent = 'Verifica';\n btn.onclick = function () { check(inp.value, p.answer); };\n inp.onkeydown = function (e) { if (e.key === 'Enter') btn.click(); };\n box.appendChild(inp); box.appendChild(btn);\n setTimeout(function () { inp.focus(); }, 50);\n } else if (p.type === 'tf') {\n ['Adevarat', 'Fals'].forEach(function (v) {\n var b = document.createElement('button');\n b.className = 'opt'; b.textContent = v;\n b.onclick = function () { check(v, p.tfAnswer); };\n box.appendChild(b);\n });\n } else {\n var correct = '';\n var opts = (p.choices || '').split('\\n').map(function (l) { return l.trim(); }).filter(Boolean);\n opts.forEach(function (o) { if (o.charAt(0) === '*') correct = o.slice(1).trim(); });\n opts.map(function (o) { return o.charAt(0) === '*' ? o.slice(1).trim() : o; })\n .forEach(function (o) {\n var b = document.createElement('button');\n b.className = 'opt'; b.textContent = o;\n b.onclick = function () { check(o, correct); };\n box.appendChild(b);\n });\n if (!opts.length) box.textContent = '(puzzle fara variante - completeaza-le in builder)';\n }\n}\n\nel('btnHint').onclick = function () {\n hintUsed = true;\n el('hinttext').style.display = 'block';\n};\n\nfunction check(given, expected) {\n if (norm(given) === norm(expected) && norm(given) !== '') {\n var stars = (hintUsed || attempts >= 2) ? 1 : (attempts === 1 ? 2 : 3);\n totalStars += stars;\n won[idx] = true;\n beep(true);\n var f = el('feedback');\n f.textContent = 'Corect! +' + stars + ' \\u2605';\n f.className = 'feedback good';\n lettersBar();\n el('bar').style.width = ((idx + 1) / CFG.puzzles.length * 100) + '%';\n setTimeout(next, 900);\n } else {\n attempts++;\n beep(false);\n var fb = el('feedback');\n fb.textContent = 'Nu e bine, mai incearca!';\n fb.className = 'feedback bad';\n var card = document.querySelector('.card');\n card.classList.remove('shake');\n void card.offsetWidth;\n card.classList.add('shake');\n }\n}\n\nfunction next() {\n idx++;\n if (idx \u003c CFG.puzzles.length) { renderPuzzle(); return; }\n if(CFG._campaign){\n var L = ''; for(var ci=0;ci\u003cCFG.puzzles.length;ci++){var lc=(CFG.puzzles[ci].letter||'').trim();if(lc)L+=lc.toUpperCase();}\n try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L.charAt(0)}); }catch(e){}\n return;\n }\n show('sFinal');\n var max = CFG.puzzles.length * 3;\n el('finalStars').textContent = totalStars + ' / ' + max + ' \\u2605';\n var word = '';\n for (var i = 0; i \u003c CFG.puzzles.length; i++) {\n var L = (CFG.puzzles[i].letter || '').trim();\n if (L) word += L.toUpperCase();\n }\n var bw = el('bigword');\n bw.innerHTML = '';\n for (var j = 0; j \u003c word.length; j++) {\n var s = document.createElement('span');\n s.textContent = word.charAt(j);\n s.style.animationDelay = (j * 0.18) + 's';\n bw.appendChild(s);\n }\n var name = CFG.player ? CFG.player + ', ' : '';\n el('finalMsg').textContent = name ? name + (CFG.finalMessage || '').charAt(0).toLowerCase() + (CFG.finalMessage || '').slice(1) : (CFG.finalMessage || '');\n confetti();\n}\n\nfunction confetti() {\n var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6'];\n for (var i = 0; i \u003c 90; i++) {\n var c = document.createElement('div');\n c.className = 'confetti';\n c.style.left = (i * 137 % 100) + 'vw';\n c.style.background = colors[i % colors.length];\n c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's';\n c.style.animationDelay = ((i * 31 % 14) / 10) + 's';\n document.body.appendChild(c);\n }\n}\n\nfunction beep(ok) {\n if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; }\n try {\n var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)());\n var t = ctx.currentTime;\n var freqs = ok ? [523, 784] : [196];\n freqs.forEach(function (f, k) {\n var o = ctx.createOscillator(), g = ctx.createGain();\n o.frequency.value = f; o.type = 'triangle';\n g.gain.setValueAtTime(0.12, t + k * 0.09);\n g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25);\n o.connect(g); g.connect(ctx.destination);\n o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3);\n });\n } catch (e) {}\n}\nwindow.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } };\nif(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} }\n\u003c/script>\n\u003c/body>\n\u003c/html>","terminal":"\u003c!doctype html>\n\u003chtml lang=\"ro\">\n\u003chead>\n\u003cmeta charset=\"utf-8\">\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\u003ctitle>\u003c/title>\n\u003cstyle>\n * { box-sizing: border-box; }\n body { margin: 0; min-height: 100vh; background: #040f08; color: #39ff6e; font-family: \"Courier New\", ui-monospace, monospace; animation: crt-flicker 6s infinite; }\n @keyframes crt-flicker { 0%,96%,100% { opacity: 1; } 97% { opacity: 1; } 98% { opacity: .94; } 99% { opacity: .98; } }\n #crt { max-width: 680px; margin: 0 auto; padding: 20px 16px 80px; }\n .line { white-space: pre-wrap; word-break: break-word; line-height: 1.45; font-size: 15px; text-shadow: 0 0 8px rgba(57,255,110,.55); }\n .line.dim { color: #2ecc71; }\n .line.warn { color: #ffd24a; text-shadow: 0 0 7px rgba(255,210,74,.45); }\n .line.bad { color: #ff6b6b; text-shadow: 0 0 7px rgba(255,107,107,.45); }\n .line.ok { color: #9dffc0; }\n #inline { display: flex; gap: 8px; align-items: baseline; font-size: 15px; min-height: 44px; text-shadow: 0 0 8px rgba(57,255,110,.55); }\n #cmd { flex: 1; min-height: 44px; background: none; border: none; outline: none; color: inherit; font: inherit; text-shadow: inherit; caret-color: #39ff6e; }\n .scan { position: fixed; inset: 0; pointer-events: none; z-index: 2; background: repeating-linear-gradient(0deg, rgba(0,0,0,.22) 0 1px, transparent 1px 3px); }\n .vign { position: fixed; inset: 0; pointer-events: none; z-index: 2; background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.6)); }\n #crt-frame { position: fixed; inset: 0; pointer-events: none; z-index: 3; border: 8px solid #0d1f12; border-radius: 18px; box-shadow: inset 0 0 60px rgba(0,0,0,.6), inset 0 0 0 1px #1a3a24; }\n @media (prefers-reduced-motion: reduce) { body { animation: none; } }\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"scan\">\u003c/div>\u003cdiv class=\"vign\">\u003c/div>\u003cdiv id=\"crt-frame\">\u003c/div>\n\u003cdiv id=\"crt\">\u003cdiv id=\"out\">\u003c/div>\n\u003cdiv id=\"inline\">\u003cspan>>\u003c/span>\u003cinput id=\"cmd\" autocomplete=\"off\" autofocus spellcheck=\"false\">\u003c/div>\n\u003c/div>\n\u003cscript>\nvar CFG = __CFG__;\ndocument.documentElement.style.setProperty('--accent', CFG.color || '#6d28d9');\nvar totalStars = 0;\nfunction el(id){ return document.getElementById(id); }\nfunction norm(s){ return String(s).trim().toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').replace(/\\s+/g, ' ').replace(/,/g, '.'); }\nfunction starsFor(att, hint){ return (hint || att >= 2) ? 1 : (att === 1 ? 2 : 3); }\nfunction finalWord(){ var w = ''; for (var i = 0; i \u003c CFG.puzzles.length; i++){ var L = (CFG.puzzles[i].letter || '').trim(); if (L) w += L.toUpperCase(); } return w; }\nfunction choiceOpts(p){ return (p.choices || '').split('\\n').map(function(l){ return l.trim(); }).filter(Boolean).map(function(o){ return o.charAt(0) === '*' ? o.slice(1).trim() : o; }); }\nfunction choiceCorrect(p){ var ls = (p.choices || '').split('\\n'); for (var i = 0; i \u003c ls.length; i++){ var l = ls[i].trim(); if (l.charAt(0) === '*') return l.slice(1).trim(); } return ''; }\nfunction checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type === 'choice' ? choiceCorrect(p) : p.answer); return norm(given) !== '' && norm(given) === norm(exp); }\nfunction beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} }\nfunction confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i \u003c 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } }\nfunction roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } }\nwindow.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } };\nif(CFG._campaign){\n /* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */\n var _cs = document.createElement('style');\n _cs.textContent = 'h1{display:none!important}.progress{display:none!important}.meta{display:none!important}';\n (document.head || document.documentElement).appendChild(_cs);\n}\nvar idx = -1, attempts = 0, hintUsed = false, done = false;\nvar solved = [];\nvar out = el('out'), cmd = el('cmd');\ndocument.body.addEventListener('click', function(){ cmd.focus(); });\n\nvar queue = [], typing = false;\nfunction say(lines, cls, cb){ queue.push({ lines: lines.slice(), cls: cls || '', cb: cb }); pump(); }\nfunction pump(){\n if (typing) return;\n var job = queue[0];\n if (!job) return;\n if (!job.lines.length) { queue.shift(); if (job.cb) job.cb(); pump(); return; }\n var text = job.lines.shift();\n typing = true;\n var d = document.createElement('div');\n d.className = 'line ' + job.cls;\n out.appendChild(d);\n var i = 0;\n (function tick(){\n d.textContent = text.slice(0, i);\n i += 3;\n window.scrollTo(0, document.body.scrollHeight);\n if (i \u003c= text.length + 2) setTimeout(tick, 11);\n else { d.textContent = text; typing = false; pump(); }\n })();\n}\nfunction echo(text, cls){ var d = document.createElement('div'); d.className = 'line ' + (cls || ''); d.textContent = text; out.appendChild(d); window.scrollTo(0, document.body.scrollHeight); }\n\nfunction collected(){ var w = ''; for (var i = 0; i \u003c CFG.puzzles.length; i++){ var L = (CFG.puzzles[i].letter || '').trim(); if (L) w += solved[i] ? L.toUpperCase() + ' ' : '_ '; } return w.trim() || '(niciuna)'; }\n\nvar bar = '==============================================';\nvar introLines = CFG._campaign\n ? [bar, ' ' + CFG.title.toUpperCase(), bar, 'Comenzi: INDICIU, AJUTOR. Scrie raspunsul si apasa Enter.']\n : [bar, ' ' + CFG.title.toUpperCase(), bar, ' ', (CFG.player ? CFG.player + ', ' : '') + CFG.story, ' ', 'Comenzi: INDICIU, LITERE, AJUTOR. In rest, scrie raspunsul si apasa Enter.'];\nsay(introLines, '', nextPuzzle);\n\nfunction nextPuzzle(){\n idx++; attempts = 0; hintUsed = false;\n if (idx >= CFG.puzzles.length) return finale();\n var p = CFG.puzzles[idx];\n var lines = [' ', '----------------------------------------------', '[' + (idx + 1) + '/' + CFG.puzzles.length + '] ' + (p.title || 'OBSTACOL').toUpperCase(), p.question];\n if (p.type === 'tf') lines.push('(raspunde: ADEVARAT sau FALS)');\n if (p.type === 'choice') { var o = choiceOpts(p); for (var i = 0; i \u003c o.length; i++) lines.push(' ' + (i + 1) + ') ' + o[i]); }\n say(lines);\n}\n\nfunction finale(){\n done = true;\n if(CFG._campaign){\n var s = totalStars; var L = finalWord().charAt(0);\n say(['>> CAMERA REZOLVATA! Stele: ' + s + (L ? ' | Litera: ' + L : '')], 'ok', function(){\n try{ parent.nextRoom({idx:CFG._campaign.idx, stars:s, letter:L}); }catch(e){}\n });\n return;\n }\n var w = finalWord().split('').join(' ');\n var lines = [' ', bar, ' E V A D A R E R E U S I T A', bar, 'Stele: ' + totalStars + ' / ' + (CFG.puzzles.length * 3)];\n if (w) lines.push('Cuvantul magic: ' + w);\n lines.push((CFG.player ? CFG.player + ', ' : '') + CFG.finalMessage);\n lines.push(' ');\n lines.push('Scrie RESTART pentru a juca din nou.');\n say(lines, 'ok');\n beep(true);\n}\n\ncmd.addEventListener('keydown', function(e){\n if (e.key !== 'Enter') return;\n var v = cmd.value.trim();\n cmd.value = '';\n if (!v) return;\n echo('> ' + v, 'dim');\n var n = norm(v);\n if (done) { if (n === 'restart') location.reload(); else echo('Scrie RESTART pentru a juca din nou.', 'dim'); return; }\n if (n === 'ajutor' || n === 'help') { say(['INDICIU = primesti un ajutor (dar pierzi stele)', 'LITERE = literele adunate pana acum', 'Orice altceva e tratat ca raspuns.']); return; }\n if (n === 'litere') { say(['Litere adunate: ' + collected()]); return; }\n var p = CFG.puzzles[idx];\n if (!p) return;\n if (n === 'indiciu' || n === 'hint') {\n if (p.hint) { hintUsed = true; say(['INDICIU: ' + p.hint], 'warn'); }\n else say(['Nu exista niciun indiciu aici.'], 'warn');\n return;\n }\n var given = v;\n if (p.type === 'choice') { var num = parseInt(v, 10); var o = choiceOpts(p); if (num >= 1 && o[num - 1]) given = o[num - 1]; }\n if (p.type === 'tf') { if (n === 'a' || n === 'adevarat') given = 'Adevarat'; if (n === 'f' || n === 'fals') given = 'Fals'; }\n if (checkAnswer(p, given)) {\n var s = starsFor(attempts, hintUsed);\n totalStars += s; solved[idx] = true; beep(true);\n var ls = ['>> ACCES PERMIS. +' + s + ' stele (total ' + totalStars + ')'];\n var L = (p.letter || '').trim();\n if (L) ls.push('>> AI GASIT LITERA: ' + L.toUpperCase() + ' [' + collected() + ']');\n say(ls, 'ok', nextPuzzle);\n } else {\n attempts++; beep(false);\n say(['>> ACCES RESPINS. Mai incearca.'], 'bad');\n }\n});\nroomReady();\n\u003c/script>\n\u003c/body>\n\u003c/html>","arcade":"\u003c!doctype html>\n\u003chtml lang=\"ro\">\n\u003chead>\n\u003cmeta charset=\"utf-8\">\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\u003ctitle>\u003c/title>\n\u003cstyle>\n * { box-sizing: border-box; }\n body { margin: 0; min-height: 100vh; background: radial-gradient(ellipse at 50% 0%, #1a0a40 0%, #080614 60%); color: #fff; font-family: ui-monospace, \"Courier New\", monospace; display: flex; flex-direction: column; align-items: center; }\n h1 { font-size: 22px; margin: 12px 0 4px; letter-spacing: .12em; text-transform: uppercase; color: #fff; text-shadow: 0 0 12px var(--accent), 0 0 24px rgba(109,40,217,.5); }\n #hud { display: flex; gap: 16px; align-items: center; font-size: 13px; color: #c4b5fd; margin-bottom: 8px; flex-wrap: wrap; justify-content: center; padding: 0 10px; }\n #hudLetters { display: flex; gap: 4px; }\n #hudLetters span { width: 32px; height: 32px; border-radius: 4px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); display: flex; align-items: center; justify-content: center; font-weight: 800; color: rgba(255,255,255,.4); font-size: 14px; }\n #hudLetters span.won { background: var(--accent); color: #fff; border-color: transparent; box-shadow: 0 0 8px var(--accent); }\n canvas { border: 4px solid var(--accent); border-radius: 4px; background: #0e0a22; max-width: calc(100vw - 16px); image-rendering: pixelated; box-shadow: 0 0 0 2px #080614, 0 0 20px rgba(109,40,217,.6), 0 0 40px rgba(109,40,217,.25), inset 0 0 30px rgba(0,0,0,.6); }\n .help { font-size: 12px; color: #8b7fc0; margin: 8px 0 4px; text-align: center; padding: 0 10px; }\n #dpad { display: flex; gap: 8px; margin: 6px 0 16px; flex-wrap: wrap; justify-content: center; }\n #dpad button { width: 56px; height: 52px; font-size: 20px; border-radius: 6px; border: 2px solid #6d28d9; background: #1a1040; color: #c4b5fd; cursor: pointer; box-shadow: 0 4px 0 #0d0820, 0 0 8px rgba(109,40,217,.3); transition: transform .08s, box-shadow .08s; }\n #dpad button:active { background: var(--accent); transform: translateY(2px); box-shadow: 0 2px 0 #0d0820, 0 0 12px var(--accent); }\n #btnBomb { background: #7f1d1d; border-color: #b91c1c; }\n @media (prefers-reduced-motion: reduce) { #dpad button { transition: none; } }\n #goOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.82); z-index: 25; align-items: center; justify-content: center; padding: 16px; }\n #goCard { background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 24px; text-align: center; max-width: 360px; font-family: system-ui, sans-serif; }\n #goCard #goMsg { font-size: 20px; margin-bottom: 14px; }\n #goCard button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 11px 18px; font-weight: 700; background: var(--accent); color: #fff; }\n\n .confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 99; animation: fall linear forwards; }\n @keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }\n .shake { animation: shake .4s ease; }\n @keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }\n #mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; }\n #mCard { width: 100%; max-width: 460px; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 22px; color: #fff; font-family: system-ui, sans-serif; box-shadow: 0 18px 50px rgba(0,0,0,.5); }\n #mCard .mtitle { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #c4b5fd; font-weight: 700; }\n #mCard .mq { font-size: 18px; line-height: 1.45; margin: 8px 0 16px; }\n #mCard input[type=text] { width: 100%; font: inherit; font-size: 17px; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(255,255,255,.25); background: rgba(0,0,0,.3); color: #fff; text-align: center; box-sizing: border-box; }\n #mCard input:focus { outline: 2px solid var(--accent); border-color: transparent; }\n #mCard button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 11px 16px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; margin-top: 10px; box-sizing: border-box; }\n #mCard button.opt { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); font-weight: 600; text-align: left; }\n #mCard button.opt:hover { background: rgba(255,255,255,.2); }\n #mCard .mfb { min-height: 20px; text-align: center; font-weight: 700; margin-top: 10px; }\n #mCard .mfb.bad { color: #fda4af; } #mCard .mfb.good { color: #86efac; }\n #mCard .mhint { background: none !important; color: rgba(255,255,255,.55) !important; font-weight: 600 !important; font-size: 13px; width: auto !important; display: block; margin: 10px auto 0; }\n #mCard .mhinttext { background: rgba(255,255,255,.1); border-radius: 9px; padding: 9px 11px; font-size: 14px; margin-top: 8px; display: none; white-space: pre-line; }\n #mCard .mclose { background: none !important; color: rgba(255,255,255,.4) !important; font-size: 12px; width: auto !important; margin: 6px auto 0; display: block; }\n #fOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.88); z-index: 30; align-items: center; justify-content: center; padding: 16px; }\n #fOverlay .fcard { width: 100%; max-width: 480px; text-align: center; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 18px; padding: 28px; color: #fff; font-family: system-ui, sans-serif; }\n #fOverlay h1 { margin: 0 0 8px; font-size: 26px; }\n #fOverlay .fstars { font-size: 26px; letter-spacing: 4px; color: #fbbf24; margin: 6px 0; }\n #fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; }\n #fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; }\n @keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }\n #fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; }\n #fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003ch1>\u003c/h1>\n\u003cdiv id=\"hud\">\u003cspan id=\"hudStep\">\u003c/span>\u003cspan id=\"hudStars\">\u003c/span>\u003cdiv id=\"hudLetters\">\u003c/div>\u003c/div>\n\u003ccanvas id=\"cv\">\u003c/canvas>\n\u003cdiv class=\"help\">Sageti / WASD = misca, Space sau 💣 = bomba. Sparge cutiile, evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.\u003c/div>\n\u003cdiv id=\"dpad\">\u003cbutton data-d=\"L\">◀\u003c/button>\u003cbutton data-d=\"U\">▲\u003c/button>\u003cbutton data-d=\"D\">▼\u003c/button>\u003cbutton data-d=\"R\">▶\u003c/button>\u003cbutton id=\"btnBomb\">💣\u003c/button>\u003c/div>\n\u003cdiv id=\"goOverlay\">\u003cdiv id=\"goCard\">\u003cdiv id=\"goMsg\">\u003c/div>\u003cbutton id=\"goRestart\">Incearca din nou\u003c/button>\u003c/div>\u003c/div>\n\u003cdiv id=\"mOverlay\">\u003cdiv id=\"mCard\">\n \u003cdiv class=\"mtitle\" id=\"mTitle\">\u003c/div>\n \u003cdiv class=\"mq\" id=\"mQ\">\u003c/div>\n \u003cdiv id=\"mAnswers\">\u003c/div>\n \u003cdiv class=\"mfb\" id=\"mFeedback\">\u003c/div>\n \u003cbutton class=\"mhint\" id=\"mHintBtn\">Vreau un indiciu\u003c/button>\n \u003cdiv class=\"mhinttext\" id=\"mHintText\">\u003c/div>\n \u003cbutton class=\"mclose\" id=\"mClose\">Pleaca de aici\u003c/button>\n\u003c/div>\u003c/div>\n\u003cdiv id=\"fOverlay\">\u003cdiv class=\"fcard\">\n \u003ch1>Evadare reusita!\u003c/h1>\n \u003cdiv class=\"fstars\" id=\"fStars\">\u003c/div>\n \u003cdiv class=\"fword\" id=\"fWord\">\u003c/div>\n \u003cp id=\"fMsg\">\u003c/p>\n \u003cbutton id=\"fAgain\">Joaca din nou\u003c/button>\n\u003c/div>\u003c/div>\n\u003cscript>\nvar CFG = __CFG__;\ndocument.documentElement.style.setProperty('--accent', CFG.color || '#6d28d9');\nvar totalStars = 0;\nfunction el(id){ return document.getElementById(id); }\nfunction norm(s){ return String(s).trim().toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').replace(/\\s+/g, ' ').replace(/,/g, '.'); }\nfunction starsFor(att, hint){ return (hint || att >= 2) ? 1 : (att === 1 ? 2 : 3); }\nfunction finalWord(){ var w = ''; for (var i = 0; i \u003c CFG.puzzles.length; i++){ var L = (CFG.puzzles[i].letter || '').trim(); if (L) w += L.toUpperCase(); } return w; }\nfunction choiceOpts(p){ return (p.choices || '').split('\\n').map(function(l){ return l.trim(); }).filter(Boolean).map(function(o){ return o.charAt(0) === '*' ? o.slice(1).trim() : o; }); }\nfunction choiceCorrect(p){ var ls = (p.choices || '').split('\\n'); for (var i = 0; i \u003c ls.length; i++){ var l = ls[i].trim(); if (l.charAt(0) === '*') return l.slice(1).trim(); } return ''; }\nfunction checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type === 'choice' ? choiceCorrect(p) : p.answer); return norm(given) !== '' && norm(given) === norm(exp); }\nfunction beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} }\nfunction confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i \u003c 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } }\nfunction roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } }\nwindow.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } };\nif(CFG._campaign){\n /* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */\n var _cs = document.createElement('style');\n _cs.textContent = 'h1{display:none!important}.progress{display:none!important}.meta{display:none!important}';\n (document.head || document.documentElement).appendChild(_cs);\n}\nvar N = CFG.puzzles.length;\n\n/* ===== Bomberman (S3 — port din scratch/bomberman-proto.html) =====\n Păstrează contractul motorului: openPuzzle/onDoorSolved/showFinal/modalOpen/roomReady. */\nvar __seed = (typeof window.__seed === 'number') ? window.__seed : (Date.now() % 0xFFFFFF);\nwindow.__seed = __seed;\nfunction makePRNG(seed){ var s = seed >>> 0; return function(){ s |= 0; s = (s + 0x6d2b79f5) | 0; var t = Math.imul(s ^ (s >>> 15), 1 | s); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }\nvar rng = makePRNG(__seed);\n\nvar GW = 15, GH = 13, TS = 36;\nvar T_FLOOR = 0, T_WALL = 1, T_BOX = 2, T_DOOR = 3, T_CHEST = 4;\nvar BOMB_TIMER = 2400, EXPLOSION_TIME = 500, EXPLOSION_RANGE = 3, ENEMY_INTERVAL = 600, RESPAWN_DELAY = 1500, INVINCIBLE_TIME = 2000, MAX_LIVES = 3;\nvar NUM_DOORS = N, NUM_ENEMIES = Math.max(2, Math.min(4, N + 1));\n\nvar map, player, enemies, bombs, explosions, lives, gameOver, gameWon, puzzleProgress, doorMeta, chestPos;\nvar animFrame, lastTime = 0, enemyTimer = 0, invincibleTimer = 0, bombIdCounter = 0;\n\nvar cv = el('cv'); cv.width = GW * TS; cv.height = GH * TS;\nvar ctx = cv.getContext('2d');\n\nfunction shuffle(arr){ for (var i = arr.length - 1; i > 0; i--){ var j = Math.floor(rng() * (i + 1)); var t = arr[i]; arr[i] = arr[j]; arr[j] = t; } return arr; }\n\nfunction buildMap(){\n map = [];\n for (var y = 0; y \u003c GH; y++){ map[y] = []; for (var x = 0; x \u003c GW; x++){ if (x === 0 || y === 0 || x === GW - 1 || y === GH - 1) map[y][x] = T_WALL; else if (x % 2 === 0 && y % 2 === 0) map[y][x] = T_WALL; else map[y][x] = T_FLOOR; } }\n var freeCells = [];\n for (var fy = 1; fy \u003c GH - 1; fy++) for (var fx = 1; fx \u003c GW - 1; fx++) if (map[fy][fx] === T_FLOOR) freeCells.push({ x: fx, y: fy });\n var safeZone = [{x:1,y:1},{x:2,y:1},{x:1,y:2}];\n function isSafe(c){ for (var i = 0; i \u003c safeZone.length; i++) if (safeZone[i].x === c.x && safeZone[i].y === c.y) return true; return false; }\n var boxCandidates = freeCells.filter(function(c){ return !isSafe(c); });\n shuffle(boxCandidates);\n var boxCount = Math.floor(boxCandidates.length * 0.55);\n for (var b = 0; b \u003c boxCount; b++) map[boxCandidates[b].y][boxCandidates[b].x] = T_BOX;\n var stillFree = [];\n for (var sy = 1; sy \u003c GH - 1; sy++) for (var sx = 1; sx \u003c GW - 1; sx++) if (map[sy][sx] === T_FLOOR && !isSafe({x:sx,y:sy})) stillFree.push({ x: sx, y: sy });\n shuffle(stillFree);\n doorMeta = [];\n for (var d = 0; d \u003c NUM_DOORS && d \u003c stillFree.length; d++){ var c = stillFree[d]; map[c.y][c.x] = T_DOOR; doorMeta.push({ x: c.x, y: c.y, id: d }); }\n var chestCandidates = [];\n for (var qy = 1; qy \u003c GH - 1; qy++) for (var qx = 1; qx \u003c GW - 1; qx++) if (map[qy][qx] === T_FLOOR && !isSafe({x:qx,y:qy})) chestCandidates.push({ x: qx, y: qy, dist: (GW - 1 - qx) + (GH - 1 - qy) });\n chestCandidates.sort(function(a,b){ return a.dist - b.dist; });\n chestPos = chestCandidates.length > 0 ? chestCandidates[0] : { x: GW - 2, y: GH - 2 };\n map[chestPos.y][chestPos.x] = T_CHEST;\n}\n\nfunction init(){\n rng = makePRNG(__seed);\n buildMap();\n player = { x: 1, y: 1, alive: true, invincible: false };\n var ec = [];\n for (var y = 1; y \u003c GH - 1; y++) for (var x = 1; x \u003c GW - 1; x++) if (map[y][x] === T_FLOOR && (x > 3 || y > 3)) ec.push({ x: x, y: y });\n shuffle(ec);\n enemies = [];\n for (var i = 0; i \u003c NUM_ENEMIES && i \u003c ec.length; i++) enemies.push({ x: ec[i].x, y: ec[i].y, alive: true, id: i });\n bombs = []; explosions = []; lives = MAX_LIVES; gameOver = false; gameWon = false; enemyTimer = 0; invincibleTimer = 0; lastTime = 0;\n if (!puzzleProgress) puzzleProgress = { doorsSolved: [] };\n for (var dd = 0; dd \u003c doorMeta.length; dd++) if (puzzleProgress.doorsSolved[dd]) map[doorMeta[dd].y][doorMeta[dd].x] = T_FLOOR;\n hideGameOver();\n updateHud();\n if (animFrame) cancelAnimationFrame(animFrame);\n animFrame = requestAnimationFrame(gameLoop);\n}\n\nfunction respawn(){\n if (lives \u003c= 0){ showGameOver(); return; }\n player = { x: 1, y: 1, alive: true, invincible: true };\n bombs = []; explosions = []; invincibleTimer = INVINCIBLE_TIME; gameOver = false;\n updateHud();\n}\n\nfunction showGameOver(){ gameOver = true; el('goMsg').textContent = '\\ud83d\\udc80 Ai ramas fara vieti!'; el('goOverlay').style.display = 'flex'; }\nfunction hideGameOver(){ el('goOverlay').style.display = 'none'; }\nel('goRestart').onclick = function(){ init(); };\n\n/* ----- HUD (motor: hudStep/hudStars/hudLetters) ----- */\nfunction updateHud(){\n var solved = puzzleProgress ? puzzleProgress.doorsSolved.filter(Boolean).length : 0;\n var alive = enemies ? enemies.filter(function(e){ return e.alive; }).length : 0;\n el('hudStep').textContent = '\\u2764\\ufe0f ' + lives + ' \\ud83d\\udc7e ' + alive + ' \\ud83d\\udd13 ' + solved + '/' + N;\n el('hudStars').textContent = totalStars + ' \\u2605';\n var hb = el('hudLetters'); hb.innerHTML = '';\n for (var j = 0; j \u003c N; j++){ var L = (CFG.puzzles[j].letter || '').trim(); if (!L) continue; var s = document.createElement('span'); if (puzzleProgress && puzzleProgress.doorsSolved[j]){ s.textContent = L.toUpperCase(); s.className = 'won'; } else s.textContent = '?'; hb.appendChild(s); }\n}\n\n/* ----- Bombe + explozii în lanț ----- */\nfunction placeBomb(){\n if (!player.alive || gameOver || gameWon || modalOpen()) return;\n if (bombs.length >= 1) return;\n bombs.push({ x: player.x, y: player.y, timer: BOMB_TIMER, id: bombIdCounter++ });\n updateHud();\n}\nfunction explodeBomb(bomb){\n bombs = bombs.filter(function(b){ return b.id !== bomb.id; });\n var cells = [{ x: bomb.x, y: bomb.y }];\n var dirs = [[1,0],[-1,0],[0,1],[0,-1]];\n for (var d = 0; d \u003c dirs.length; d++){ var dx = dirs[d][0], dy = dirs[d][1]; for (var r = 1; r \u003c= EXPLOSION_RANGE; r++){ var cx = bomb.x + dx * r, cy = bomb.y + dy * r; if (cx \u003c 0 || cy \u003c 0 || cx >= GW || cy >= GH) break; var t = map[cy][cx]; if (t === T_WALL) break; cells.push({ x: cx, y: cy }); if (t === T_BOX){ map[cy][cx] = T_FLOOR; break; } if (t === T_DOOR || t === T_CHEST) break; } }\n explosions.push({ cells: cells, endTime: performance.now() + EXPLOSION_TIME });\n var chain = bombs.slice();\n for (var i = 0; i \u003c chain.length; i++){ var bb = chain[i]; for (var c = 0; c \u003c cells.length; c++) if (cells[c].x === bb.x && cells[c].y === bb.y){ explodeBomb(bb); break; } }\n checkExplosionHits(cells);\n updateHud();\n}\nfunction checkExplosionHits(cells){\n for (var c = 0; c \u003c cells.length; c++){ var cx = cells[c].x, cy = cells[c].y; for (var i = 0; i \u003c enemies.length; i++) if (enemies[i].alive && enemies[i].x === cx && enemies[i].y === cy) enemies[i].alive = false; if (player.alive && !player.invincible && player.x === cx && player.y === cy) killPlayer(); }\n}\nfunction killPlayer(){\n if (!player.alive) return;\n player.alive = false; lives--; updateHud();\n setTimeout(function(){ if (lives > 0) respawn(); else showGameOver(); }, RESPAWN_DELAY);\n}\n\n/* ----- Mișcare jucător + uși (puzzle) / cufăr (scăpare) ----- */\nfunction movePlayer(dir){\n if (!player.alive || gameOver || gameWon || modalOpen()) return;\n var dx = 0, dy = 0;\n if (dir === 'U') dy = -1; else if (dir === 'D') dy = 1; else if (dir === 'L') dx = -1; else if (dir === 'R') dx = 1;\n var nx = player.x + dx, ny = player.y + dy;\n if (nx \u003c 0 || ny \u003c 0 || nx >= GW || ny >= GH) return;\n var t = map[ny][nx];\n if (t === T_WALL || t === T_BOX) return;\n for (var i = 0; i \u003c bombs.length; i++) if (bombs[i].x === nx && bombs[i].y === ny) return;\n if (t === T_DOOR){ for (var d = 0; d \u003c doorMeta.length; d++) if (doorMeta[d].x === nx && doorMeta[d].y === ny){ if (!puzzleProgress.doorsSolved[d]) openPuzzle(d, onDoorSolved); return; } return; }\n if (t === T_CHEST){ player.x = nx; player.y = ny; gameWon = true; showFinal(); return; }\n player.x = nx; player.y = ny;\n checkPlayerEnemyCollision();\n}\nfunction onDoorSolved(id){\n if (!puzzleProgress) puzzleProgress = { doorsSolved: [] };\n puzzleProgress.doorsSolved[id] = true;\n if (doorMeta && doorMeta[id]) map[doorMeta[id].y][doorMeta[id].x] = T_FLOOR;\n updateHud();\n}\n\n/* ----- AI dușmani: BFS spre jucător (doar pe podea) ----- */\nfunction moveEnemies(){ if (gameOver || gameWon) return; for (var i = 0; i \u003c enemies.length; i++){ var e = enemies[i]; if (!e.alive) continue; var next = bfsStep(e.x, e.y, player.x, player.y); if (next){ e.x = next.x; e.y = next.y; } } checkPlayerEnemyCollision(); }\nfunction bfsStep(sx, sy, tx, ty){\n if (sx === tx && sy === ty) return null;\n var visited = {}; var queue = [{ x: sx, y: sy, step: null }]; visited[sy + ',' + sx] = true;\n var dirs = [[1,0],[-1,0],[0,1],[0,-1]];\n while (queue.length > 0){ var cur = queue.shift(); for (var d = 0; d \u003c dirs.length; d++){ var nx = cur.x + dirs[d][0], ny = cur.y + dirs[d][1]; var key = ny + ',' + nx; if (nx \u003c 0 || ny \u003c 0 || nx >= GW || ny >= GH) continue; if (visited[key]) continue; if (map[ny][nx] !== T_FLOOR) continue; var hb = false; for (var bi = 0; bi \u003c bombs.length; bi++) if (bombs[bi].x === nx && bombs[bi].y === ny){ hb = true; break; } if (hb) continue; var he = false; for (var ei = 0; ei \u003c enemies.length; ei++) if (enemies[ei].alive && enemies[ei].x === nx && enemies[ei].y === ny){ he = true; break; } if (he) continue; visited[key] = true; var step = cur.step || { x: nx, y: ny }; if (nx === tx && ny === ty) return step; queue.push({ x: nx, y: ny, step: step }); } }\n return null;\n}\nfunction checkPlayerEnemyCollision(){ if (!player.alive || player.invincible) return; for (var i = 0; i \u003c enemies.length; i++) if (enemies[i].alive && enemies[i].x === player.x && enemies[i].y === player.y){ killPlayer(); return; } }\n\n/* ----- Game loop ----- */\nfunction gameLoop(now){\n var dt = now - (lastTime || now); lastTime = now;\n if (!gameOver && !gameWon){\n if (player.invincible && invincibleTimer > 0){ invincibleTimer -= dt; if (invincibleTimer \u003c= 0){ player.invincible = false; invincibleTimer = 0; checkPlayerEnemyCollision(); } }\n var explodeList = [];\n for (var i = 0; i \u003c bombs.length; i++){ bombs[i].timer -= dt; if (bombs[i].timer \u003c= 0) explodeList.push(bombs[i]); }\n for (var k = 0; k \u003c explodeList.length; k++) explodeBomb(explodeList[k]);\n var nowMs = performance.now();\n explosions = explosions.filter(function(ex){ return ex.endTime > nowMs; });\n if (!modalOpen() && player.alive){ enemyTimer += dt; if (enemyTimer >= ENEMY_INTERVAL){ enemyTimer = 0; moveEnemies(); } }\n }\n draw(now); updateHud();\n animFrame = requestAnimationFrame(gameLoop);\n}\n\n/* ----- Desenare ----- */\nfunction draw(now){\n ctx.clearRect(0, 0, cv.width, cv.height);\n var expSet = {}; var nowMs = performance.now();\n for (var ex = 0; ex \u003c explosions.length; ex++) if (explosions[ex].endTime > nowMs){ var cs = explosions[ex].cells; for (var c = 0; c \u003c cs.length; c++) expSet[cs[c].y + ',' + cs[c].x] = true; }\n for (var y = 0; y \u003c GH; y++) for (var x = 0; x \u003c GW; x++){ var px = x * TS, py = y * TS, t = map[y][x], isExp = expSet[y + ',' + x];\n if (t === T_WALL) drawWall(px, py, y);\n else if (t === T_BOX) drawBox(px, py, isExp);\n else if (t === T_DOOR){ drawFloor(px, py, x, y, isExp); drawDoor(px, py); }\n else if (t === T_CHEST){ drawFloor(px, py, x, y, isExp); drawChest(px, py); }\n else drawFloor(px, py, x, y, isExp);\n }\n for (var bi = 0; bi \u003c bombs.length; bi++) drawBomb(bombs[bi], now);\n for (var en = 0; en \u003c enemies.length; en++) if (enemies[en].alive) drawEnemy(enemies[en]);\n if (player.alive) drawPlayer(now);\n}\nfunction drawWall(px, py, y){ ctx.fillStyle = '#33215f'; ctx.fillRect(px, py, TS, TS); ctx.fillStyle = '#241646'; ctx.fillRect(px, py + TS/2 - 1, TS, 2); ctx.fillRect(px + ((y%2) ? TS/2 : TS/4) - 1, py, 2, TS/2 - 1); ctx.fillRect(px + ((y%2) ? TS/4 : 3*TS/4) - 1, py + TS/2, 2, TS/2); }\nfunction drawFloor(px, py, x, y, isExp){ if (isExp){ ctx.fillStyle = '#f97316'; ctx.fillRect(px, py, TS, TS); ctx.fillStyle = '#fef08a'; ctx.fillRect(px + TS/4, py + TS/4, TS/2, TS/2); } else { ctx.fillStyle = ((x + y) % 2) ? '#191130' : '#1c1336'; ctx.fillRect(px, py, TS, TS); } }\nfunction drawBox(px, py, isExp){ if (isExp){ ctx.fillStyle = '#f97316'; ctx.fillRect(px, py, TS, TS); return; } ctx.fillStyle = '#78350f'; ctx.fillRect(px, py, TS, TS); ctx.fillStyle = '#92400e'; ctx.fillRect(px+2, py+2, TS-4, TS-4); ctx.strokeStyle = '#d97706'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(px+4, py+4); ctx.lineTo(px+TS-4, py+TS-4); ctx.moveTo(px+TS-4, py+4); ctx.lineTo(px+4, py+TS-4); ctx.stroke(); }\nfunction drawDoor(px, py){ ctx.fillStyle = '#9f1239'; ctx.fillRect(px+3, py+2, TS-6, TS-4); ctx.fillStyle = '#e11d48'; ctx.fillRect(px+6, py+5, TS-12, TS-10); ctx.fillStyle = '#fbbf24'; ctx.fillRect(px+TS/2-2, py+TS/2-2, 4, 7); }\nfunction drawChest(px, py){ ctx.fillStyle = '#92400e'; ctx.fillRect(px+4, py+8, TS-8, TS-14); ctx.fillStyle = '#f59e0b'; ctx.fillRect(px+4, py+8, TS-8, 8); ctx.fillStyle = '#fde68a'; ctx.fillRect(px+TS/2-3, py+11, 6, 10); ctx.fillStyle = '#fbbf24'; ctx.fillRect(px+TS/2-2, py+13, 4, 4); }\nfunction drawBomb(bomb, now){ var px = bomb.x * TS, py = bomb.y * TS; var pulse = Math.sin(now / 150) * 0.3 + 0.7; ctx.fillStyle = 'rgba(30,10,10,' + pulse + ')'; ctx.beginPath(); ctx.arc(px + TS/2, py + TS/2, TS/2 - 4, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#f87171'; ctx.lineWidth = 2; ctx.stroke(); ctx.strokeStyle = '#f59e0b'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(px + TS/2 + 4, py + 6); ctx.quadraticCurveTo(px + TS/2 + 10, py + 2, px + TS/2 + 7, py - 2); ctx.stroke(); }\nfunction drawEnemy(e){ var px = e.x * TS, py = e.y * TS; ctx.fillStyle = '#dc2626'; ctx.fillRect(px+6, py+8, TS-12, TS-12); ctx.fillStyle = '#fff'; ctx.fillRect(px+9, py+12, 4, 4); ctx.fillRect(px+TS-13, py+12, 4, 4); ctx.fillStyle = '#000'; ctx.fillRect(px+10, py+13, 2, 2); ctx.fillRect(px+TS-12, py+13, 2, 2); ctx.fillStyle = '#b91c1c'; ctx.fillRect(px+8, py+TS-8, 4, 5); ctx.fillRect(px+TS-12, py+TS-8, 4, 5); }\nfunction drawPlayer(now){ var px = player.x * TS, py = player.y * TS; if (player.invincible && Math.floor(now / 100) % 2 === 0) return; ctx.fillStyle = CFG.color || '#6d28d9'; ctx.fillRect(px+6, py+5, TS-12, TS-10); ctx.fillStyle = '#fff'; ctx.fillRect(px+10, py+10, 4, 4); ctx.fillRect(px+TS-14, py+10, 4, 4); ctx.fillStyle = '#000'; ctx.fillRect(px+11, py+11, 2, 2); ctx.fillRect(px+TS-13, py+11, 2, 2); ctx.fillStyle = '#fff'; ctx.fillRect(px+11, py+TS-12, TS-22, 3); }\n\n/* ----- Input ----- */\nwindow.addEventListener('keydown', function(e){\n if (modalOpen()) return;\n var dir = { ArrowUp:'U', ArrowDown:'D', ArrowLeft:'L', ArrowRight:'R', w:'U', s:'D', a:'L', d:'R' }[e.key];\n if (dir){ e.preventDefault(); movePlayer(dir); return; }\n if (e.key === ' ' || e.key === 'b' || e.key === 'B'){ e.preventDefault(); placeBomb(); }\n});\ndocument.querySelectorAll('#dpad button[data-d]').forEach(function(b){ b.addEventListener('click', function(){ movePlayer(b.dataset.d); }); });\nel('btnBomb').addEventListener('click', function(){ placeBomb(); });\n\n/* ----- Hooks de test (window.__game) ----- */\nwindow.__game = {\n get lives(){ return lives; },\n get enemiesCount(){ return enemies ? enemies.filter(function(e){ return e.alive; }).length : 0; },\n get puzzleProgress(){ return puzzleProgress; },\n get bombs(){ return bombs ? bombs.slice() : []; },\n get gameOver(){ return gameOver; },\n get gameWon(){ return gameWon; },\n get player(){ return player ? { x: player.x, y: player.y, alive: player.alive, invincible: player.invincible } : null; },\n get map(){ return map ? map.map(function(r){ return r.slice(); }) : []; },\n get enemies(){ return enemies ? enemies.slice() : []; },\n get explosions(){ return explosions ? explosions.slice() : []; },\n placeBomb: function(){ placeBomb(); },\n movePlayer: function(dir){ movePlayer(dir); },\n explodeAllBombs: function(){ var list = bombs.slice(); for (var i = 0; i \u003c list.length; i++) explodeBomb(list[i]); },\n spawnEnemyAt: function(x, y){ enemies.push({ x: x, y: y, alive: true, id: 999 + enemies.length }); },\n killPlayer: function(){ killPlayer(); },\n restartWithSeed: function(seed){ __seed = seed; window.__seed = seed; puzzleProgress = null; init(); },\n getDoorAt: function(x, y){ for (var d = 0; d \u003c doorMeta.length; d++) if (doorMeta[d].x === x && doorMeta[d].y === y) return d; return -1; },\n solveDoor: function(id){ onDoorSolved(id); },\n teleportPlayer: function(x, y){ player.x = x; player.y = y; },\n bfsStep: function(sx, sy, tx, ty){ return bfsStep(sx, sy, tx, ty); },\n setTile: function(x, y, t){ if (map && map[y]) map[y][x] = t; },\n getTile: function(x, y){ return map && map[y] ? map[y][x] : -1; }\n};\n\nvar mIdx = -1, mAtt = 0, mHint = false, mCb = null;\nel('mHintBtn').onclick = function(){ mHint = true; el('mHintText').style.display = 'block'; };\nel('mClose').onclick = function(){ el('mOverlay').style.display = 'none'; };\nfunction modalOpen(){ return el('mOverlay').style.display === 'flex'; }\nfunction openPuzzle(i, cb){\n mIdx = i; mAtt = 0; mHint = false; mCb = cb;\n var p = CFG.puzzles[i];\n el('mTitle').textContent = p.title || ('Puzzle ' + (i + 1));\n el('mQ').textContent = p.question;\n el('mFeedback').textContent = ''; el('mFeedback').className = 'mfb';\n el('mHintText').style.display = 'none'; el('mHintText').textContent = p.hint || '';\n el('mHintBtn').style.display = p.hint ? '' : 'none';\n var box = el('mAnswers'); box.innerHTML = '';\n if (p.type === 'free') {\n var inp = document.createElement('input'); inp.type = 'text'; inp.placeholder = 'Scrie raspunsul...'; inp.autocomplete = 'off';\n var b = document.createElement('button'); b.textContent = 'Verifica';\n b.onclick = function(){ mCheck(inp.value); };\n inp.onkeydown = function(e){ e.stopPropagation(); if (e.key === 'Enter') b.click(); };\n box.appendChild(inp); box.appendChild(b);\n setTimeout(function(){ inp.focus(); }, 60);\n } else if (p.type === 'tf') {\n ['Adevarat', 'Fals'].forEach(function(v){ var b = document.createElement('button'); b.className = 'opt'; b.textContent = v; b.onclick = function(){ mCheck(v); }; box.appendChild(b); });\n } else {\n choiceOpts(p).forEach(function(o){ var b = document.createElement('button'); b.className = 'opt'; b.textContent = o; b.onclick = function(){ mCheck(o); }; box.appendChild(b); });\n }\n el('mOverlay').style.display = 'flex';\n}\nfunction mCheck(given){\n var p = CFG.puzzles[mIdx];\n if (checkAnswer(p, given)) {\n var s = starsFor(mAtt, mHint);\n totalStars += s; beep(true);\n el('mFeedback').textContent = 'Corect! +' + s + ' \\u2605'; el('mFeedback').className = 'mfb good';\n setTimeout(function(){ el('mOverlay').style.display = 'none'; var cb = mCb; mCb = null; if (cb) cb(mIdx, s); }, 750);\n } else {\n mAtt++; beep(false);\n el('mFeedback').textContent = 'Nu e bine, mai incearca!'; el('mFeedback').className = 'mfb bad';\n var c = el('mCard'); c.classList.remove('shake'); void c.offsetWidth; c.classList.add('shake');\n }\n}\nfunction showFinal(){\n if(CFG._campaign){\n var L = finalWord().charAt(0);\n try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L}); }catch(e){}\n return;\n }\n el('fStars').textContent = totalStars + ' / ' + (CFG.puzzles.length * 3) + ' \\u2605';\n var w = finalWord(); var bw = el('fWord'); bw.innerHTML = '';\n for (var j = 0; j \u003c w.length; j++){ var s = document.createElement('span'); s.textContent = w.charAt(j); s.style.animationDelay = (j * 0.18) + 's'; bw.appendChild(s); }\n var msg = CFG.finalMessage || '';\n el('fMsg').textContent = CFG.player ? CFG.player + ', ' + msg.charAt(0).toLowerCase() + msg.slice(1) : msg;\n el('fOverlay').style.display = 'flex';\n beep(true); confetti();\n}\nel('fAgain').onclick = function(){ location.reload(); };\ninit();\nroomReady();\n\u003c/script>\n\u003c/body>\n\u003c/html>"}; +var TPL = {"classic":"\u003c!doctype html>\n\u003chtml lang=\"ro\">\n\u003chead>\n\u003cmeta charset=\"utf-8\">\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\u003ctitle>\u003c/title>\n\u003cstyle>\n * { box-sizing: border-box; }\n body {\n margin: 0; min-height: 100vh; font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n color: #f1f0ff; display: flex; align-items: center; justify-content: center; padding: 16px;\n background: radial-gradient(ellipse at 50% 30%, #2a0e5e 0%, #0e0622 70%);\n }\n .card {\n width: 100%; max-width: 560px; background: #1a0e3d;\n border: 1px solid rgba(255,255,255,.18); border-radius: 20px; padding: 26px;\n backdrop-filter: blur(6px);\n box-shadow: 0 0 0 1px rgba(255,255,255,.06), 0 24px 60px rgba(0,0,0,.55), 0 0 40px rgba(109,40,217,.35);\n }\n h1 { margin: 0 0 6px; font-size: 26px; text-align: center; }\n .story { color: rgba(255,255,255,.8); text-align: center; line-height: 1.5; }\n .screen { display: none; }\n .screen.on { display: block; animation: pop .35s cubic-bezier(.22,1,.36,1); }\n @keyframes pop { from { transform: scale(.94) translateY(6px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }\n .progress { height: 10px; background: rgba(255,255,255,.12); border-radius: 99px; overflow: hidden; margin: 14px 0 4px; }\n .progress i { display: block; height: 100%; background: var(--accent); width: 0; box-shadow: 0 0 8px var(--accent); transition: width .5s cubic-bezier(.22,1,.36,1); }\n .meta { display: flex; justify-content: space-between; font-size: 12px; color: rgba(255,255,255,.6); margin-bottom: 14px; }\n .letters { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; margin: 14px 0; }\n .tile {\n width: 44px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center;\n font-weight: 800; font-size: 20px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.18);\n color: rgba(255,255,255,.35);\n }\n .tile.won { background: var(--accent); color: #fff; border-color: transparent; box-shadow: 0 0 12px var(--accent); animation: flip .5s cubic-bezier(.34,1.56,.64,1); }\n @keyframes flip { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }\n .qtitle { font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--accent-light); font-weight: 700; }\n .question { font-size: 21px; line-height: 1.5; margin: 8px 0 18px; color: #f1f0ff; }\n input[type=text] {\n width: 100%; font: inherit; font-size: 18px; padding: 11px 13px; border-radius: 10px;\n border: 1px solid rgba(255,255,255,.25); background: rgba(0,0,0,.25); color: #fff; text-align: center;\n }\n input:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-color: transparent; }\n button {\n font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px;\n font-weight: 700; background: var(--accent); color: #fff; width: 100%; margin-top: 10px; min-height: 44px;\n }\n button:hover { filter: brightness(1.12); }\n button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n button.opt { background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.16); font-weight: 600; text-align: left; min-height: 48px; transition: background .15s, border-color .15s; }\n button.opt:hover { background: rgba(255,255,255,.16); border-color: var(--accent); }\n button.hint { background: none; border: none; color: rgba(255,255,255,.55); font-weight: 600; font-size: 13px; width: auto; display: block; margin: 12px auto 0; }\n button.hint:hover { color: #fff; }\n .hinttext { background: rgba(255,255,255,.1); border-radius: 9px; padding: 10px 12px; font-size: 14px; margin-top: 10px; white-space: pre-line; display: none; }\n .feedback { min-height: 22px; text-align: center; font-weight: 700; margin-top: 10px; }\n .feedback.bad { color: #fda4af; }\n .feedback.good { color: #86efac; }\n .shake { animation: shake .4s ease; }\n @keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }\n .stars { text-align: center; font-size: 26px; letter-spacing: 4px; color: #fbbf24; margin: 6px 0; }\n .bigword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 18px 0; }\n .bigword span {\n width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex;\n align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flip .6s ease backwards;\n }\n .confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 5; animation: fall linear forwards; }\n @keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }\n @media (prefers-reduced-motion: reduce) {\n .screen.on, .tile.won, .bigword span, .shake { animation: none; }\n .progress i { transition: none; }\n }\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"card\">\n \u003cdiv id=\"sStart\" class=\"screen on\">\n \u003ch1 id=\"gtitle\">\u003c/h1>\n \u003cp class=\"story\" id=\"gstory\">\u003c/p>\n \u003cbutton id=\"btnStart\">Incepe aventura\u003c/button>\n \u003c/div>\n\n \u003cdiv id=\"sGame\" class=\"screen\">\n \u003cdiv class=\"progress\">\u003ci id=\"bar\">\u003c/i>\u003c/div>\n \u003cdiv class=\"meta\">\u003cspan id=\"step\">\u003c/span>\u003cspan id=\"score\">\u003c/span>\u003c/div>\n \u003cdiv class=\"letters\" id=\"lettersBar\">\u003c/div>\n \u003cdiv id=\"qbox\">\n \u003cdiv class=\"qtitle\" id=\"qtitle\">\u003c/div>\n \u003cdiv class=\"question\" id=\"qtext\">\u003c/div>\n \u003cdiv id=\"answers\">\u003c/div>\n \u003cdiv class=\"feedback\" id=\"feedback\">\u003c/div>\n \u003cbutton class=\"hint\" id=\"btnHint\">Vreau un indiciu\u003c/button>\n \u003cdiv class=\"hinttext\" id=\"hinttext\">\u003c/div>\n \u003c/div>\n \u003c/div>\n\n \u003cdiv id=\"sFinal\" class=\"screen\">\n \u003ch1>Evadare reusita!\u003c/h1>\n \u003cdiv class=\"stars\" id=\"finalStars\">\u003c/div>\n \u003cdiv class=\"bigword\" id=\"bigword\">\u003c/div>\n \u003cp class=\"story\" id=\"finalMsg\">\u003c/p>\n \u003cbutton id=\"btnAgain\">Joaca din nou\u003c/button>\n \u003c/div>\n\u003c/div>\n\n\u003cscript>\nvar CFG = __CFG__;\ndocument.documentElement.style.setProperty('--accent', CFG.color || '#6d28d9');\ndocument.documentElement.style.setProperty('--accent-light', 'color-mix(in srgb, ' + (CFG.color || '#6d28d9') + ' 40%, white)');\n\nvar idx = 0, totalStars = 0, attempts = 0, hintUsed = false, won = [];\n\nfunction el(id) { return document.getElementById(id); }\nfunction norm(s) {\n return String(s).trim().toLowerCase().normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '').replace(/\\s+/g, ' ').replace(/,/g, '.');\n}\nfunction show(id) {\n var scr = document.querySelectorAll('.screen');\n for (var i = 0; i \u003c scr.length; i++) scr[i].classList.remove('on');\n el(id).classList.add('on');\n}\n\nel('gtitle').textContent = CFG.title;\nvar hello = CFG.player ? 'Salut, ' + CFG.player + '! ' : '';\nel('gstory').textContent = hello + CFG.story;\n\nel('btnStart').onclick = function () { show('sGame'); renderPuzzle(); };\nel('btnAgain').onclick = function () { location.reload(); };\n\nfunction lettersBar() {\n var bar = el('lettersBar');\n bar.innerHTML = '';\n var any = false;\n for (var i = 0; i \u003c CFG.puzzles.length; i++) {\n var L = (CFG.puzzles[i].letter || '').trim();\n if (!L) continue;\n any = true;\n var d = document.createElement('div');\n d.className = 'tile' + (won[i] ? ' won' : '');\n d.textContent = won[i] ? L.toUpperCase() : '?';\n bar.appendChild(d);\n }\n bar.style.display = any ? '' : 'none';\n}\n\nfunction renderPuzzle() {\n var p = CFG.puzzles[idx];\n attempts = 0; hintUsed = false;\n el('bar').style.width = (idx / CFG.puzzles.length * 100) + '%';\n el('step').textContent = 'Puzzle ' + (idx + 1) + ' din ' + CFG.puzzles.length;\n el('score').textContent = totalStars + ' \\u2605';\n el('qtitle').textContent = p.title || 'Puzzle ' + (idx + 1);\n el('qtext').textContent = p.question;\n el('feedback').textContent = ''; el('feedback').className = 'feedback';\n el('hinttext').style.display = 'none';\n el('hinttext').textContent = p.hint || '';\n el('btnHint').style.display = p.hint ? '' : 'none';\n lettersBar();\n\n var box = el('answers');\n box.innerHTML = '';\n if (p.type === 'free') {\n var inp = document.createElement('input');\n inp.type = 'text'; inp.autocomplete = 'off'; inp.placeholder = 'Scrie raspunsul...';\n var btn = document.createElement('button');\n btn.textContent = 'Verifica';\n btn.onclick = function () { check(inp.value, p.answer); };\n inp.onkeydown = function (e) { if (e.key === 'Enter') btn.click(); };\n box.appendChild(inp); box.appendChild(btn);\n setTimeout(function () { inp.focus(); }, 50);\n } else if (p.type === 'tf') {\n ['Adevarat', 'Fals'].forEach(function (v) {\n var b = document.createElement('button');\n b.className = 'opt'; b.textContent = v;\n b.onclick = function () { check(v, p.tfAnswer); };\n box.appendChild(b);\n });\n } else {\n var correct = '';\n var opts = (p.choices || '').split('\\n').map(function (l) { return l.trim(); }).filter(Boolean);\n opts.forEach(function (o) { if (o.charAt(0) === '*') correct = o.slice(1).trim(); });\n opts.map(function (o) { return o.charAt(0) === '*' ? o.slice(1).trim() : o; })\n .forEach(function (o) {\n var b = document.createElement('button');\n b.className = 'opt'; b.textContent = o;\n b.onclick = function () { check(o, correct); };\n box.appendChild(b);\n });\n if (!opts.length) box.textContent = '(puzzle fara variante - completeaza-le in builder)';\n }\n}\n\nel('btnHint').onclick = function () {\n hintUsed = true;\n el('hinttext').style.display = 'block';\n};\n\nfunction check(given, expected) {\n if (norm(given) === norm(expected) && norm(given) !== '') {\n var stars = (hintUsed || attempts >= 2) ? 1 : (attempts === 1 ? 2 : 3);\n totalStars += stars;\n won[idx] = true;\n beep(true);\n var f = el('feedback');\n f.textContent = 'Corect! +' + stars + ' \\u2605';\n f.className = 'feedback good';\n lettersBar();\n el('bar').style.width = ((idx + 1) / CFG.puzzles.length * 100) + '%';\n setTimeout(next, 900);\n } else {\n attempts++;\n beep(false);\n var fb = el('feedback');\n fb.textContent = 'Nu e bine, mai incearca!';\n fb.className = 'feedback bad';\n var card = document.querySelector('.card');\n card.classList.remove('shake');\n void card.offsetWidth;\n card.classList.add('shake');\n }\n}\n\nfunction next() {\n idx++;\n if (idx \u003c CFG.puzzles.length) { renderPuzzle(); return; }\n if(CFG._campaign){\n var L = ''; for(var ci=0;ci\u003cCFG.puzzles.length;ci++){var lc=(CFG.puzzles[ci].letter||'').trim();if(lc)L+=lc.toUpperCase();}\n try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L.charAt(0)}); }catch(e){}\n return;\n }\n show('sFinal');\n var max = CFG.puzzles.length * 3;\n el('finalStars').textContent = totalStars + ' / ' + max + ' \\u2605';\n var word = '';\n for (var i = 0; i \u003c CFG.puzzles.length; i++) {\n var L = (CFG.puzzles[i].letter || '').trim();\n if (L) word += L.toUpperCase();\n }\n var bw = el('bigword');\n bw.innerHTML = '';\n for (var j = 0; j \u003c word.length; j++) {\n var s = document.createElement('span');\n s.textContent = word.charAt(j);\n s.style.animationDelay = (j * 0.18) + 's';\n bw.appendChild(s);\n }\n var name = CFG.player ? CFG.player + ', ' : '';\n el('finalMsg').textContent = name ? name + (CFG.finalMessage || '').charAt(0).toLowerCase() + (CFG.finalMessage || '').slice(1) : (CFG.finalMessage || '');\n confetti();\n}\n\nfunction confetti() {\n var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6'];\n for (var i = 0; i \u003c 90; i++) {\n var c = document.createElement('div');\n c.className = 'confetti';\n c.style.left = (i * 137 % 100) + 'vw';\n c.style.background = colors[i % colors.length];\n c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's';\n c.style.animationDelay = ((i * 31 % 14) / 10) + 's';\n document.body.appendChild(c);\n }\n}\n\nfunction beep(ok) {\n if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; }\n try {\n var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)());\n var t = ctx.currentTime;\n var freqs = ok ? [523, 784] : [196];\n freqs.forEach(function (f, k) {\n var o = ctx.createOscillator(), g = ctx.createGain();\n o.frequency.value = f; o.type = 'triangle';\n g.gain.setValueAtTime(0.12, t + k * 0.09);\n g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25);\n o.connect(g); g.connect(ctx.destination);\n o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3);\n });\n } catch (e) {}\n}\nwindow.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } };\nif(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} }\n\u003c/script>\n\u003c/body>\n\u003c/html>","terminal":"\u003c!doctype html>\n\u003chtml lang=\"ro\">\n\u003chead>\n\u003cmeta charset=\"utf-8\">\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\u003ctitle>\u003c/title>\n\u003cstyle>\n * { box-sizing: border-box; }\n body { margin: 0; min-height: 100vh; background: #040f08; color: #39ff6e; font-family: \"Courier New\", ui-monospace, monospace; animation: crt-flicker 6s infinite; }\n @keyframes crt-flicker { 0%,96%,100% { opacity: 1; } 97% { opacity: 1; } 98% { opacity: .94; } 99% { opacity: .98; } }\n #crt { max-width: 680px; margin: 0 auto; padding: 20px 16px 80px; }\n .line { white-space: pre-wrap; word-break: break-word; line-height: 1.45; font-size: 15px; text-shadow: 0 0 8px rgba(57,255,110,.55); }\n .line.dim { color: #2ecc71; }\n .line.warn { color: #ffd24a; text-shadow: 0 0 7px rgba(255,210,74,.45); }\n .line.bad { color: #ff6b6b; text-shadow: 0 0 7px rgba(255,107,107,.45); }\n .line.ok { color: #9dffc0; }\n #inline { display: flex; gap: 8px; align-items: baseline; font-size: 15px; min-height: 44px; text-shadow: 0 0 8px rgba(57,255,110,.55); }\n #cmd { flex: 1; min-height: 44px; background: none; border: none; outline: none; color: inherit; font: inherit; text-shadow: inherit; caret-color: #39ff6e; }\n .scan { position: fixed; inset: 0; pointer-events: none; z-index: 2; background: repeating-linear-gradient(0deg, rgba(0,0,0,.22) 0 1px, transparent 1px 3px); }\n .vign { position: fixed; inset: 0; pointer-events: none; z-index: 2; background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.6)); }\n #crt-frame { position: fixed; inset: 0; pointer-events: none; z-index: 3; border: 8px solid #0d1f12; border-radius: 18px; box-shadow: inset 0 0 60px rgba(0,0,0,.6), inset 0 0 0 1px #1a3a24; }\n @media (prefers-reduced-motion: reduce) { body { animation: none; } }\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003cdiv class=\"scan\">\u003c/div>\u003cdiv class=\"vign\">\u003c/div>\u003cdiv id=\"crt-frame\">\u003c/div>\n\u003cdiv id=\"crt\">\u003cdiv id=\"out\">\u003c/div>\n\u003cdiv id=\"inline\">\u003cspan>>\u003c/span>\u003cinput id=\"cmd\" autocomplete=\"off\" autofocus spellcheck=\"false\">\u003c/div>\n\u003c/div>\n\u003cscript>\nvar CFG = __CFG__;\ndocument.documentElement.style.setProperty('--accent', CFG.color || '#6d28d9');\nvar totalStars = 0;\nfunction el(id){ return document.getElementById(id); }\nfunction norm(s){ return String(s).trim().toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').replace(/\\s+/g, ' ').replace(/,/g, '.'); }\nfunction starsFor(att, hint){ return (hint || att >= 2) ? 1 : (att === 1 ? 2 : 3); }\nfunction finalWord(){ var w = ''; for (var i = 0; i \u003c CFG.puzzles.length; i++){ var L = (CFG.puzzles[i].letter || '').trim(); if (L) w += L.toUpperCase(); } return w; }\nfunction choiceOpts(p){ return (p.choices || '').split('\\n').map(function(l){ return l.trim(); }).filter(Boolean).map(function(o){ return o.charAt(0) === '*' ? o.slice(1).trim() : o; }); }\nfunction choiceCorrect(p){ var ls = (p.choices || '').split('\\n'); for (var i = 0; i \u003c ls.length; i++){ var l = ls[i].trim(); if (l.charAt(0) === '*') return l.slice(1).trim(); } return ''; }\nfunction checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type === 'choice' ? choiceCorrect(p) : p.answer); return norm(given) !== '' && norm(given) === norm(exp); }\nfunction beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} }\nfunction confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i \u003c 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } }\nfunction roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } }\n/* Contract de finalizare a camerei — un singur loc pentru payload-ul parent.nextRoom\n (înlocuiește duplicatele din showFinal/finale; D7). Citește totalStars + finalWord() la apel. */\nfunction campaignDone(){ if(CFG._campaign){ try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:finalWord().charAt(0)}); }catch(e){} } }\nwindow.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } };\nif(CFG._campaign){\n /* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */\n var _cs = document.createElement('style');\n _cs.textContent = 'h1{display:none!important}.progress{display:none!important}.meta{display:none!important}';\n (document.head || document.documentElement).appendChild(_cs);\n}\nvar idx = -1, attempts = 0, hintUsed = false, done = false;\nvar solved = [];\nvar out = el('out'), cmd = el('cmd');\ndocument.body.addEventListener('click', function(){ cmd.focus(); });\n\nvar queue = [], typing = false;\nfunction say(lines, cls, cb){ queue.push({ lines: lines.slice(), cls: cls || '', cb: cb }); pump(); }\nfunction pump(){\n if (typing) return;\n var job = queue[0];\n if (!job) return;\n if (!job.lines.length) { queue.shift(); if (job.cb) job.cb(); pump(); return; }\n var text = job.lines.shift();\n typing = true;\n var d = document.createElement('div');\n d.className = 'line ' + job.cls;\n out.appendChild(d);\n var i = 0;\n (function tick(){\n d.textContent = text.slice(0, i);\n i += 3;\n window.scrollTo(0, document.body.scrollHeight);\n if (i \u003c= text.length + 2) setTimeout(tick, 11);\n else { d.textContent = text; typing = false; pump(); }\n })();\n}\nfunction echo(text, cls){ var d = document.createElement('div'); d.className = 'line ' + (cls || ''); d.textContent = text; out.appendChild(d); window.scrollTo(0, document.body.scrollHeight); }\n\nfunction collected(){ var w = ''; for (var i = 0; i \u003c CFG.puzzles.length; i++){ var L = (CFG.puzzles[i].letter || '').trim(); if (L) w += solved[i] ? L.toUpperCase() + ' ' : '_ '; } return w.trim() || '(niciuna)'; }\n\nvar bar = '==============================================';\nvar introLines = CFG._campaign\n ? [bar, ' ' + CFG.title.toUpperCase(), bar, 'Comenzi: INDICIU, AJUTOR. Scrie raspunsul si apasa Enter.']\n : [bar, ' ' + CFG.title.toUpperCase(), bar, ' ', (CFG.player ? CFG.player + ', ' : '') + CFG.story, ' ', 'Comenzi: INDICIU, LITERE, AJUTOR. In rest, scrie raspunsul si apasa Enter.'];\nsay(introLines, '', nextPuzzle);\n\nfunction nextPuzzle(){\n idx++; attempts = 0; hintUsed = false;\n if (idx >= CFG.puzzles.length) return finale();\n var p = CFG.puzzles[idx];\n var lines = [' ', '----------------------------------------------', '[' + (idx + 1) + '/' + CFG.puzzles.length + '] ' + (p.title || 'OBSTACOL').toUpperCase(), p.question];\n if (p.type === 'tf') lines.push('(raspunde: ADEVARAT sau FALS)');\n if (p.type === 'choice') { var o = choiceOpts(p); for (var i = 0; i \u003c o.length; i++) lines.push(' ' + (i + 1) + ') ' + o[i]); }\n say(lines);\n}\n\nfunction finale(){\n done = true;\n if(CFG._campaign){\n var s = totalStars; var L = finalWord().charAt(0);\n say(['>> CAMERA REZOLVATA! Stele: ' + s + (L ? ' | Litera: ' + L : '')], 'ok', campaignDone);\n return;\n }\n var w = finalWord().split('').join(' ');\n var lines = [' ', bar, ' E V A D A R E R E U S I T A', bar, 'Stele: ' + totalStars + ' / ' + (CFG.puzzles.length * 3)];\n if (w) lines.push('Cuvantul magic: ' + w);\n lines.push((CFG.player ? CFG.player + ', ' : '') + CFG.finalMessage);\n lines.push(' ');\n lines.push('Scrie RESTART pentru a juca din nou.');\n say(lines, 'ok');\n beep(true);\n}\n\ncmd.addEventListener('keydown', function(e){\n if (e.key !== 'Enter') return;\n var v = cmd.value.trim();\n cmd.value = '';\n if (!v) return;\n echo('> ' + v, 'dim');\n var n = norm(v);\n if (done) { if (n === 'restart') location.reload(); else echo('Scrie RESTART pentru a juca din nou.', 'dim'); return; }\n if (n === 'ajutor' || n === 'help') { say(['INDICIU = primesti un ajutor (dar pierzi stele)', 'LITERE = literele adunate pana acum', 'Orice altceva e tratat ca raspuns.']); return; }\n if (n === 'litere') { say(['Litere adunate: ' + collected()]); return; }\n var p = CFG.puzzles[idx];\n if (!p) return;\n if (n === 'indiciu' || n === 'hint') {\n if (p.hint) { hintUsed = true; say(['INDICIU: ' + p.hint], 'warn'); }\n else say(['Nu exista niciun indiciu aici.'], 'warn');\n return;\n }\n var given = v;\n if (p.type === 'choice') { var num = parseInt(v, 10); var o = choiceOpts(p); if (num >= 1 && o[num - 1]) given = o[num - 1]; }\n if (p.type === 'tf') { if (n === 'a' || n === 'adevarat') given = 'Adevarat'; if (n === 'f' || n === 'fals') given = 'Fals'; }\n if (checkAnswer(p, given)) {\n var s = starsFor(attempts, hintUsed);\n totalStars += s; solved[idx] = true; beep(true);\n var ls = ['>> ACCES PERMIS. +' + s + ' stele (total ' + totalStars + ')'];\n var L = (p.letter || '').trim();\n if (L) ls.push('>> AI GASIT LITERA: ' + L.toUpperCase() + ' [' + collected() + ']');\n say(ls, 'ok', nextPuzzle);\n } else {\n attempts++; beep(false);\n say(['>> ACCES RESPINS. Mai incearca.'], 'bad');\n }\n});\nroomReady();\n\u003c/script>\n\u003c/body>\n\u003c/html>","arcade":"\u003c!doctype html>\n\u003chtml lang=\"ro\">\n\u003chead>\n\u003cmeta charset=\"utf-8\">\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\u003ctitle>\u003c/title>\n\u003cstyle>\n * { box-sizing: border-box; }\n body { margin: 0; min-height: 100vh; background: radial-gradient(ellipse at 50% 0%, #1a0a40 0%, #080614 60%); color: #fff; font-family: ui-monospace, \"Courier New\", monospace; display: flex; flex-direction: column; align-items: center; }\n h1 { font-size: 22px; margin: 12px 0 4px; letter-spacing: .12em; text-transform: uppercase; color: #fff; text-shadow: 0 0 12px var(--accent), 0 0 24px rgba(109,40,217,.5); }\n #hud { display: flex; gap: 16px; align-items: center; font-size: 13px; color: #c4b5fd; margin-bottom: 8px; flex-wrap: wrap; justify-content: center; padding: 0 10px; }\n #hudLetters { display: flex; gap: 4px; }\n #hudLetters span { width: 32px; height: 32px; border-radius: 4px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); display: flex; align-items: center; justify-content: center; font-weight: 800; color: rgba(255,255,255,.4); font-size: 14px; }\n #hudLetters span.won { background: var(--accent); color: #fff; border-color: transparent; box-shadow: 0 0 8px var(--accent); }\n canvas { border: 4px solid var(--accent); border-radius: 4px; background: #0e0a22; max-width: calc(100vw - 16px); image-rendering: pixelated; box-shadow: 0 0 0 2px #080614, 0 0 20px rgba(109,40,217,.6), 0 0 40px rgba(109,40,217,.25), inset 0 0 30px rgba(0,0,0,.6); }\n .help { font-size: 12px; color: #8b7fc0; margin: 8px 0 4px; text-align: center; padding: 0 10px; }\n #dpad { display: flex; gap: 8px; margin: 6px 0 16px; flex-wrap: wrap; justify-content: center; }\n #dpad button { width: 56px; height: 52px; font-size: 20px; border-radius: 6px; border: 2px solid #6d28d9; background: #1a1040; color: #c4b5fd; cursor: pointer; box-shadow: 0 4px 0 #0d0820, 0 0 8px rgba(109,40,217,.3); transition: transform .08s, box-shadow .08s; }\n #dpad button:active { background: var(--accent); transform: translateY(2px); box-shadow: 0 2px 0 #0d0820, 0 0 12px var(--accent); }\n #btnBomb { background: #7f1d1d; border-color: #b91c1c; }\n @media (prefers-reduced-motion: reduce) { #dpad button { transition: none; } }\n #goOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.82); z-index: 25; align-items: center; justify-content: center; padding: 16px; }\n #goCard { background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 24px; text-align: center; max-width: 360px; font-family: system-ui, sans-serif; }\n #goCard #goMsg { font-size: 20px; margin-bottom: 14px; }\n #goCard button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 11px 18px; font-weight: 700; background: var(--accent); color: #fff; }\n\n .confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 99; animation: fall linear forwards; }\n @keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }\n .shake { animation: shake .4s ease; }\n @keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }\n #mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; }\n #mCard { width: 100%; max-width: 460px; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 22px; color: #fff; font-family: system-ui, sans-serif; box-shadow: 0 18px 50px rgba(0,0,0,.5); }\n #mCard .mtitle { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #c4b5fd; font-weight: 700; }\n #mCard .mq { font-size: 18px; line-height: 1.45; margin: 8px 0 16px; }\n #mCard input[type=text] { width: 100%; font: inherit; font-size: 17px; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(255,255,255,.25); background: rgba(0,0,0,.3); color: #fff; text-align: center; box-sizing: border-box; }\n #mCard input:focus { outline: 2px solid var(--accent); border-color: transparent; }\n #mCard button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 11px 16px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; margin-top: 10px; box-sizing: border-box; }\n #mCard button.opt { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); font-weight: 600; text-align: left; }\n #mCard button.opt:hover { background: rgba(255,255,255,.2); }\n #mCard .mfb { min-height: 20px; text-align: center; font-weight: 700; margin-top: 10px; }\n #mCard .mfb.bad { color: #fda4af; } #mCard .mfb.good { color: #86efac; }\n #mCard .mhint { background: none !important; color: rgba(255,255,255,.55) !important; font-weight: 600 !important; font-size: 13px; width: auto !important; display: block; margin: 10px auto 0; }\n #mCard .mhinttext { background: rgba(255,255,255,.1); border-radius: 9px; padding: 9px 11px; font-size: 14px; margin-top: 8px; display: none; white-space: pre-line; }\n #mCard .mclose { background: none !important; color: rgba(255,255,255,.4) !important; font-size: 12px; width: auto !important; margin: 6px auto 0; display: block; }\n #fOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.88); z-index: 30; align-items: center; justify-content: center; padding: 16px; }\n #fOverlay .fcard { width: 100%; max-width: 480px; text-align: center; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 18px; padding: 28px; color: #fff; font-family: system-ui, sans-serif; }\n #fOverlay h1 { margin: 0 0 8px; font-size: 26px; }\n #fOverlay .fstars { font-size: 26px; letter-spacing: 4px; color: #fbbf24; margin: 6px 0; }\n #fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; }\n #fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; }\n @keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }\n #fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; }\n #fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }\n\u003c/style>\n\u003c/head>\n\u003cbody>\n\u003ch1>\u003c/h1>\n\u003cdiv id=\"hud\">\u003cspan id=\"hudStep\">\u003c/span>\u003cspan id=\"hudStars\">\u003c/span>\u003cdiv id=\"hudLetters\">\u003c/div>\u003c/div>\n\u003ccanvas id=\"cv\">\u003c/canvas>\n\u003cdiv class=\"help\">Sageti / WASD = misca, Space sau 💣 = bomba. Sparge cutiile, evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.\u003c/div>\n\u003cdiv id=\"dpad\">\u003cbutton data-d=\"L\">◀\u003c/button>\u003cbutton data-d=\"U\">▲\u003c/button>\u003cbutton data-d=\"D\">▼\u003c/button>\u003cbutton data-d=\"R\">▶\u003c/button>\u003cbutton id=\"btnBomb\">💣\u003c/button>\u003c/div>\n\u003cdiv id=\"goOverlay\">\u003cdiv id=\"goCard\">\u003cdiv id=\"goMsg\">\u003c/div>\u003cbutton id=\"goRestart\">Incearca din nou\u003c/button>\u003c/div>\u003c/div>\n\u003cdiv id=\"mOverlay\">\u003cdiv id=\"mCard\">\n \u003cdiv class=\"mtitle\" id=\"mTitle\">\u003c/div>\n \u003cdiv class=\"mq\" id=\"mQ\">\u003c/div>\n \u003cdiv id=\"mAnswers\">\u003c/div>\n \u003cdiv class=\"mfb\" id=\"mFeedback\">\u003c/div>\n \u003cbutton class=\"mhint\" id=\"mHintBtn\">Vreau un indiciu\u003c/button>\n \u003cdiv class=\"mhinttext\" id=\"mHintText\">\u003c/div>\n \u003cbutton class=\"mclose\" id=\"mClose\">Pleaca de aici\u003c/button>\n\u003c/div>\u003c/div>\n\u003cdiv id=\"fOverlay\">\u003cdiv class=\"fcard\">\n \u003ch1>Evadare reusita!\u003c/h1>\n \u003cdiv class=\"fstars\" id=\"fStars\">\u003c/div>\n \u003cdiv class=\"fword\" id=\"fWord\">\u003c/div>\n \u003cp id=\"fMsg\">\u003c/p>\n \u003cbutton id=\"fAgain\">Joaca din nou\u003c/button>\n\u003c/div>\u003c/div>\n\u003cscript>\nvar CFG = __CFG__;\ndocument.documentElement.style.setProperty('--accent', CFG.color || '#6d28d9');\nvar totalStars = 0;\nfunction el(id){ return document.getElementById(id); }\nfunction norm(s){ return String(s).trim().toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').replace(/\\s+/g, ' ').replace(/,/g, '.'); }\nfunction starsFor(att, hint){ return (hint || att >= 2) ? 1 : (att === 1 ? 2 : 3); }\nfunction finalWord(){ var w = ''; for (var i = 0; i \u003c CFG.puzzles.length; i++){ var L = (CFG.puzzles[i].letter || '').trim(); if (L) w += L.toUpperCase(); } return w; }\nfunction choiceOpts(p){ return (p.choices || '').split('\\n').map(function(l){ return l.trim(); }).filter(Boolean).map(function(o){ return o.charAt(0) === '*' ? o.slice(1).trim() : o; }); }\nfunction choiceCorrect(p){ var ls = (p.choices || '').split('\\n'); for (var i = 0; i \u003c ls.length; i++){ var l = ls[i].trim(); if (l.charAt(0) === '*') return l.slice(1).trim(); } return ''; }\nfunction checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type === 'choice' ? choiceCorrect(p) : p.answer); return norm(given) !== '' && norm(given) === norm(exp); }\nfunction beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} }\nfunction confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i \u003c 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } }\nfunction roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } }\n/* Contract de finalizare a camerei — un singur loc pentru payload-ul parent.nextRoom\n (înlocuiește duplicatele din showFinal/finale; D7). Citește totalStars + finalWord() la apel. */\nfunction campaignDone(){ if(CFG._campaign){ try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:finalWord().charAt(0)}); }catch(e){} } }\nwindow.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } };\nif(CFG._campaign){\n /* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */\n var _cs = document.createElement('style');\n _cs.textContent = 'h1{display:none!important}.progress{display:none!important}.meta{display:none!important}';\n (document.head || document.documentElement).appendChild(_cs);\n}\nvar N = CFG.puzzles.length;\n\n/* ===== Bomberman (S3 — port din scratch/bomberman-proto.html) =====\n Păstrează contractul motorului: openPuzzle/onDoorSolved/showFinal/modalOpen/roomReady. */\nvar __seed = (typeof window.__seed === 'number') ? window.__seed : (Date.now() % 0xFFFFFF);\nwindow.__seed = __seed;\nfunction makePRNG(seed){ var s = seed >>> 0; return function(){ s |= 0; s = (s + 0x6d2b79f5) | 0; var t = Math.imul(s ^ (s >>> 15), 1 | s); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }\nvar rng = makePRNG(__seed);\n\nvar GW = 15, GH = 13, TS = 36;\nvar T_FLOOR = 0, T_WALL = 1, T_BOX = 2, T_DOOR = 3, T_CHEST = 4;\nvar BOMB_TIMER = 2400, EXPLOSION_TIME = 500, EXPLOSION_RANGE = 3, ENEMY_INTERVAL = 600, RESPAWN_DELAY = 1500, INVINCIBLE_TIME = 2000, MAX_LIVES = 3;\nvar NUM_DOORS = N, NUM_ENEMIES = Math.max(2, Math.min(4, N + 1));\n\nvar map, player, enemies, bombs, explosions, lives, gameOver, gameWon, puzzleProgress, doorMeta, chestPos;\nvar animFrame, lastTime = 0, enemyTimer = 0, invincibleTimer = 0, bombIdCounter = 0;\n\nvar cv = el('cv'); cv.width = GW * TS; cv.height = GH * TS;\nvar ctx = cv.getContext('2d');\n\nfunction shuffle(arr){ for (var i = arr.length - 1; i > 0; i--){ var j = Math.floor(rng() * (i + 1)); var t = arr[i]; arr[i] = arr[j]; arr[j] = t; } return arr; }\n\nfunction buildMap(){\n map = [];\n for (var y = 0; y \u003c GH; y++){ map[y] = []; for (var x = 0; x \u003c GW; x++){ if (x === 0 || y === 0 || x === GW - 1 || y === GH - 1) map[y][x] = T_WALL; else if (x % 2 === 0 && y % 2 === 0) map[y][x] = T_WALL; else map[y][x] = T_FLOOR; } }\n var freeCells = [];\n for (var fy = 1; fy \u003c GH - 1; fy++) for (var fx = 1; fx \u003c GW - 1; fx++) if (map[fy][fx] === T_FLOOR) freeCells.push({ x: fx, y: fy });\n var safeZone = [{x:1,y:1},{x:2,y:1},{x:1,y:2}];\n function isSafe(c){ for (var i = 0; i \u003c safeZone.length; i++) if (safeZone[i].x === c.x && safeZone[i].y === c.y) return true; return false; }\n var boxCandidates = freeCells.filter(function(c){ return !isSafe(c); });\n shuffle(boxCandidates);\n var boxCount = Math.floor(boxCandidates.length * 0.55);\n for (var b = 0; b \u003c boxCount; b++) map[boxCandidates[b].y][boxCandidates[b].x] = T_BOX;\n var stillFree = [];\n for (var sy = 1; sy \u003c GH - 1; sy++) for (var sx = 1; sx \u003c GW - 1; sx++) if (map[sy][sx] === T_FLOOR && !isSafe({x:sx,y:sy})) stillFree.push({ x: sx, y: sy });\n shuffle(stillFree);\n doorMeta = [];\n for (var d = 0; d \u003c NUM_DOORS && d \u003c stillFree.length; d++){ var c = stillFree[d]; map[c.y][c.x] = T_DOOR; doorMeta.push({ x: c.x, y: c.y, id: d }); }\n var chestCandidates = [];\n for (var qy = 1; qy \u003c GH - 1; qy++) for (var qx = 1; qx \u003c GW - 1; qx++) if (map[qy][qx] === T_FLOOR && !isSafe({x:qx,y:qy})) chestCandidates.push({ x: qx, y: qy, dist: (GW - 1 - qx) + (GH - 1 - qy) });\n chestCandidates.sort(function(a,b){ return a.dist - b.dist; });\n chestPos = chestCandidates.length > 0 ? chestCandidates[0] : { x: GW - 2, y: GH - 2 };\n map[chestPos.y][chestPos.x] = T_CHEST;\n}\n\nfunction init(){\n rng = makePRNG(__seed);\n buildMap();\n player = { x: 1, y: 1, alive: true, invincible: false };\n var ec = [];\n for (var y = 1; y \u003c GH - 1; y++) for (var x = 1; x \u003c GW - 1; x++) if (map[y][x] === T_FLOOR && (x > 3 || y > 3)) ec.push({ x: x, y: y });\n shuffle(ec);\n enemies = [];\n for (var i = 0; i \u003c NUM_ENEMIES && i \u003c ec.length; i++) enemies.push({ x: ec[i].x, y: ec[i].y, alive: true, id: i });\n bombs = []; explosions = []; lives = MAX_LIVES; gameOver = false; gameWon = false; enemyTimer = 0; invincibleTimer = 0; lastTime = 0;\n if (!puzzleProgress) puzzleProgress = { doorsSolved: [] };\n for (var dd = 0; dd \u003c doorMeta.length; dd++) if (puzzleProgress.doorsSolved[dd]) map[doorMeta[dd].y][doorMeta[dd].x] = T_FLOOR;\n hideGameOver();\n updateHud();\n if (animFrame) cancelAnimationFrame(animFrame);\n animFrame = requestAnimationFrame(gameLoop);\n}\n\nfunction respawn(){\n if (lives \u003c= 0){ showGameOver(); return; }\n player = { x: 1, y: 1, alive: true, invincible: true };\n bombs = []; explosions = []; invincibleTimer = INVINCIBLE_TIME; gameOver = false;\n updateHud();\n}\n\nfunction showGameOver(){ gameOver = true; el('goMsg').textContent = '\\ud83d\\udc80 Ai ramas fara vieti!'; el('goOverlay').style.display = 'flex'; }\nfunction hideGameOver(){ el('goOverlay').style.display = 'none'; }\nel('goRestart').onclick = function(){ init(); };\n\n/* ----- HUD (motor: hudStep/hudStars/hudLetters) ----- */\nfunction updateHud(){\n var solved = puzzleProgress ? puzzleProgress.doorsSolved.filter(Boolean).length : 0;\n var alive = enemies ? enemies.filter(function(e){ return e.alive; }).length : 0;\n el('hudStep').textContent = '\\u2764\\ufe0f ' + lives + ' \\ud83d\\udc7e ' + alive + ' \\ud83d\\udd13 ' + solved + '/' + N;\n el('hudStars').textContent = totalStars + ' \\u2605';\n var hb = el('hudLetters'); hb.innerHTML = '';\n for (var j = 0; j \u003c N; j++){ var L = (CFG.puzzles[j].letter || '').trim(); if (!L) continue; var s = document.createElement('span'); if (puzzleProgress && puzzleProgress.doorsSolved[j]){ s.textContent = L.toUpperCase(); s.className = 'won'; } else s.textContent = '?'; hb.appendChild(s); }\n}\n\n/* ----- Bombe + explozii în lanț ----- */\nfunction placeBomb(){\n if (!player.alive || gameOver || gameWon || modalOpen()) return;\n if (bombs.length >= 1) return;\n bombs.push({ x: player.x, y: player.y, timer: BOMB_TIMER, id: bombIdCounter++ });\n updateHud();\n}\nfunction explodeBomb(bomb){\n bombs = bombs.filter(function(b){ return b.id !== bomb.id; });\n var cells = [{ x: bomb.x, y: bomb.y }];\n var dirs = [[1,0],[-1,0],[0,1],[0,-1]];\n for (var d = 0; d \u003c dirs.length; d++){ var dx = dirs[d][0], dy = dirs[d][1]; for (var r = 1; r \u003c= EXPLOSION_RANGE; r++){ var cx = bomb.x + dx * r, cy = bomb.y + dy * r; if (cx \u003c 0 || cy \u003c 0 || cx >= GW || cy >= GH) break; var t = map[cy][cx]; if (t === T_WALL) break; cells.push({ x: cx, y: cy }); if (t === T_BOX){ map[cy][cx] = T_FLOOR; break; } if (t === T_DOOR || t === T_CHEST) break; } }\n explosions.push({ cells: cells, endTime: performance.now() + EXPLOSION_TIME });\n var chain = bombs.slice();\n for (var i = 0; i \u003c chain.length; i++){ var bb = chain[i]; for (var c = 0; c \u003c cells.length; c++) if (cells[c].x === bb.x && cells[c].y === bb.y){ explodeBomb(bb); break; } }\n checkExplosionHits(cells);\n updateHud();\n}\nfunction checkExplosionHits(cells){\n for (var c = 0; c \u003c cells.length; c++){ var cx = cells[c].x, cy = cells[c].y; for (var i = 0; i \u003c enemies.length; i++) if (enemies[i].alive && enemies[i].x === cx && enemies[i].y === cy) enemies[i].alive = false; if (player.alive && !player.invincible && player.x === cx && player.y === cy) killPlayer(); }\n}\nfunction killPlayer(){\n if (!player.alive) return;\n player.alive = false; lives--; updateHud();\n setTimeout(function(){ if (lives > 0) respawn(); else showGameOver(); }, RESPAWN_DELAY);\n}\n\n/* ----- Mișcare jucător + uși (puzzle) / cufăr (scăpare) ----- */\nfunction movePlayer(dir){\n if (!player.alive || gameOver || gameWon || modalOpen()) return;\n var dx = 0, dy = 0;\n if (dir === 'U') dy = -1; else if (dir === 'D') dy = 1; else if (dir === 'L') dx = -1; else if (dir === 'R') dx = 1;\n var nx = player.x + dx, ny = player.y + dy;\n if (nx \u003c 0 || ny \u003c 0 || nx >= GW || ny >= GH) return;\n var t = map[ny][nx];\n if (t === T_WALL || t === T_BOX) return;\n for (var i = 0; i \u003c bombs.length; i++) if (bombs[i].x === nx && bombs[i].y === ny) return;\n if (t === T_DOOR){ for (var d = 0; d \u003c doorMeta.length; d++) if (doorMeta[d].x === nx && doorMeta[d].y === ny){ if (!puzzleProgress.doorsSolved[d]) openPuzzle(d, onDoorSolved); return; } return; }\n if (t === T_CHEST){ player.x = nx; player.y = ny; gameWon = true; showFinal(); return; }\n player.x = nx; player.y = ny;\n checkPlayerEnemyCollision();\n}\nfunction onDoorSolved(id){\n if (!puzzleProgress) puzzleProgress = { doorsSolved: [] };\n puzzleProgress.doorsSolved[id] = true;\n if (doorMeta && doorMeta[id]) map[doorMeta[id].y][doorMeta[id].x] = T_FLOOR;\n updateHud();\n}\n\n/* ----- AI dușmani: BFS spre jucător (doar pe podea) ----- */\nfunction moveEnemies(){ if (gameOver || gameWon) return; for (var i = 0; i \u003c enemies.length; i++){ var e = enemies[i]; if (!e.alive) continue; var next = bfsStep(e.x, e.y, player.x, player.y); if (next){ e.x = next.x; e.y = next.y; } } checkPlayerEnemyCollision(); }\nfunction bfsStep(sx, sy, tx, ty){\n if (sx === tx && sy === ty) return null;\n var visited = {}; var queue = [{ x: sx, y: sy, step: null }]; visited[sy + ',' + sx] = true;\n var dirs = [[1,0],[-1,0],[0,1],[0,-1]];\n while (queue.length > 0){ var cur = queue.shift(); for (var d = 0; d \u003c dirs.length; d++){ var nx = cur.x + dirs[d][0], ny = cur.y + dirs[d][1]; var key = ny + ',' + nx; if (nx \u003c 0 || ny \u003c 0 || nx >= GW || ny >= GH) continue; if (visited[key]) continue; if (map[ny][nx] !== T_FLOOR) continue; var hb = false; for (var bi = 0; bi \u003c bombs.length; bi++) if (bombs[bi].x === nx && bombs[bi].y === ny){ hb = true; break; } if (hb) continue; var he = false; for (var ei = 0; ei \u003c enemies.length; ei++) if (enemies[ei].alive && enemies[ei].x === nx && enemies[ei].y === ny){ he = true; break; } if (he) continue; visited[key] = true; var step = cur.step || { x: nx, y: ny }; if (nx === tx && ny === ty) return step; queue.push({ x: nx, y: ny, step: step }); } }\n return null;\n}\nfunction checkPlayerEnemyCollision(){ if (!player.alive || player.invincible) return; for (var i = 0; i \u003c enemies.length; i++) if (enemies[i].alive && enemies[i].x === player.x && enemies[i].y === player.y){ killPlayer(); return; } }\n\n/* ----- Game loop ----- */\nfunction gameLoop(now){\n var dt = now - (lastTime || now); lastTime = now;\n if (!gameOver && !gameWon){\n if (player.invincible && invincibleTimer > 0){ invincibleTimer -= dt; if (invincibleTimer \u003c= 0){ player.invincible = false; invincibleTimer = 0; checkPlayerEnemyCollision(); } }\n var explodeList = [];\n for (var i = 0; i \u003c bombs.length; i++){ bombs[i].timer -= dt; if (bombs[i].timer \u003c= 0) explodeList.push(bombs[i]); }\n for (var k = 0; k \u003c explodeList.length; k++) explodeBomb(explodeList[k]);\n var nowMs = performance.now();\n explosions = explosions.filter(function(ex){ return ex.endTime > nowMs; });\n if (!modalOpen() && player.alive){ enemyTimer += dt; if (enemyTimer >= ENEMY_INTERVAL){ enemyTimer = 0; moveEnemies(); } }\n }\n draw(now); updateHud();\n animFrame = requestAnimationFrame(gameLoop);\n}\n\n/* ----- Desenare ----- */\nfunction draw(now){\n ctx.clearRect(0, 0, cv.width, cv.height);\n var expSet = {}; var nowMs = performance.now();\n for (var ex = 0; ex \u003c explosions.length; ex++) if (explosions[ex].endTime > nowMs){ var cs = explosions[ex].cells; for (var c = 0; c \u003c cs.length; c++) expSet[cs[c].y + ',' + cs[c].x] = true; }\n for (var y = 0; y \u003c GH; y++) for (var x = 0; x \u003c GW; x++){ var px = x * TS, py = y * TS, t = map[y][x], isExp = expSet[y + ',' + x];\n if (t === T_WALL) drawWall(px, py, y);\n else if (t === T_BOX) drawBox(px, py, isExp);\n else if (t === T_DOOR){ drawFloor(px, py, x, y, isExp); drawDoor(px, py); }\n else if (t === T_CHEST){ drawFloor(px, py, x, y, isExp); drawChest(px, py); }\n else drawFloor(px, py, x, y, isExp);\n }\n for (var bi = 0; bi \u003c bombs.length; bi++) drawBomb(bombs[bi], now);\n for (var en = 0; en \u003c enemies.length; en++) if (enemies[en].alive) drawEnemy(enemies[en]);\n if (player.alive) drawPlayer(now);\n}\nfunction drawWall(px, py, y){ ctx.fillStyle = '#33215f'; ctx.fillRect(px, py, TS, TS); ctx.fillStyle = '#241646'; ctx.fillRect(px, py + TS/2 - 1, TS, 2); ctx.fillRect(px + ((y%2) ? TS/2 : TS/4) - 1, py, 2, TS/2 - 1); ctx.fillRect(px + ((y%2) ? TS/4 : 3*TS/4) - 1, py + TS/2, 2, TS/2); }\nfunction drawFloor(px, py, x, y, isExp){ if (isExp){ ctx.fillStyle = '#f97316'; ctx.fillRect(px, py, TS, TS); ctx.fillStyle = '#fef08a'; ctx.fillRect(px + TS/4, py + TS/4, TS/2, TS/2); } else { ctx.fillStyle = ((x + y) % 2) ? '#191130' : '#1c1336'; ctx.fillRect(px, py, TS, TS); } }\nfunction drawBox(px, py, isExp){ if (isExp){ ctx.fillStyle = '#f97316'; ctx.fillRect(px, py, TS, TS); return; } ctx.fillStyle = '#78350f'; ctx.fillRect(px, py, TS, TS); ctx.fillStyle = '#92400e'; ctx.fillRect(px+2, py+2, TS-4, TS-4); ctx.strokeStyle = '#d97706'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(px+4, py+4); ctx.lineTo(px+TS-4, py+TS-4); ctx.moveTo(px+TS-4, py+4); ctx.lineTo(px+4, py+TS-4); ctx.stroke(); }\nfunction drawDoor(px, py){ ctx.fillStyle = '#9f1239'; ctx.fillRect(px+3, py+2, TS-6, TS-4); ctx.fillStyle = '#e11d48'; ctx.fillRect(px+6, py+5, TS-12, TS-10); ctx.fillStyle = '#fbbf24'; ctx.fillRect(px+TS/2-2, py+TS/2-2, 4, 7); }\nfunction drawChest(px, py){ ctx.fillStyle = '#92400e'; ctx.fillRect(px+4, py+8, TS-8, TS-14); ctx.fillStyle = '#f59e0b'; ctx.fillRect(px+4, py+8, TS-8, 8); ctx.fillStyle = '#fde68a'; ctx.fillRect(px+TS/2-3, py+11, 6, 10); ctx.fillStyle = '#fbbf24'; ctx.fillRect(px+TS/2-2, py+13, 4, 4); }\nfunction drawBomb(bomb, now){ var px = bomb.x * TS, py = bomb.y * TS; var pulse = Math.sin(now / 150) * 0.3 + 0.7; ctx.fillStyle = 'rgba(30,10,10,' + pulse + ')'; ctx.beginPath(); ctx.arc(px + TS/2, py + TS/2, TS/2 - 4, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#f87171'; ctx.lineWidth = 2; ctx.stroke(); ctx.strokeStyle = '#f59e0b'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(px + TS/2 + 4, py + 6); ctx.quadraticCurveTo(px + TS/2 + 10, py + 2, px + TS/2 + 7, py - 2); ctx.stroke(); }\nfunction drawEnemy(e){ var px = e.x * TS, py = e.y * TS; ctx.fillStyle = '#dc2626'; ctx.fillRect(px+6, py+8, TS-12, TS-12); ctx.fillStyle = '#fff'; ctx.fillRect(px+9, py+12, 4, 4); ctx.fillRect(px+TS-13, py+12, 4, 4); ctx.fillStyle = '#000'; ctx.fillRect(px+10, py+13, 2, 2); ctx.fillRect(px+TS-12, py+13, 2, 2); ctx.fillStyle = '#b91c1c'; ctx.fillRect(px+8, py+TS-8, 4, 5); ctx.fillRect(px+TS-12, py+TS-8, 4, 5); }\nfunction drawPlayer(now){ var px = player.x * TS, py = player.y * TS; if (player.invincible && Math.floor(now / 100) % 2 === 0) return; ctx.fillStyle = CFG.color || '#6d28d9'; ctx.fillRect(px+6, py+5, TS-12, TS-10); ctx.fillStyle = '#fff'; ctx.fillRect(px+10, py+10, 4, 4); ctx.fillRect(px+TS-14, py+10, 4, 4); ctx.fillStyle = '#000'; ctx.fillRect(px+11, py+11, 2, 2); ctx.fillRect(px+TS-13, py+11, 2, 2); ctx.fillStyle = '#fff'; ctx.fillRect(px+11, py+TS-12, TS-22, 3); }\n\n/* ----- Input ----- */\nwindow.addEventListener('keydown', function(e){\n if (modalOpen()) return;\n var dir = { ArrowUp:'U', ArrowDown:'D', ArrowLeft:'L', ArrowRight:'R', w:'U', s:'D', a:'L', d:'R' }[e.key];\n if (dir){ e.preventDefault(); movePlayer(dir); return; }\n if (e.key === ' ' || e.key === 'b' || e.key === 'B'){ e.preventDefault(); placeBomb(); }\n});\ndocument.querySelectorAll('#dpad button[data-d]').forEach(function(b){ b.addEventListener('click', function(){ movePlayer(b.dataset.d); }); });\nel('btnBomb').addEventListener('click', function(){ placeBomb(); });\n\n/* ----- Hooks de test (window.__game) ----- */\nwindow.__game = {\n get lives(){ return lives; },\n get enemiesCount(){ return enemies ? enemies.filter(function(e){ return e.alive; }).length : 0; },\n get puzzleProgress(){ return puzzleProgress; },\n get bombs(){ return bombs ? bombs.slice() : []; },\n get gameOver(){ return gameOver; },\n get gameWon(){ return gameWon; },\n get player(){ return player ? { x: player.x, y: player.y, alive: player.alive, invincible: player.invincible } : null; },\n get map(){ return map ? map.map(function(r){ return r.slice(); }) : []; },\n get enemies(){ return enemies ? enemies.slice() : []; },\n get explosions(){ return explosions ? explosions.slice() : []; },\n placeBomb: function(){ placeBomb(); },\n movePlayer: function(dir){ movePlayer(dir); },\n explodeAllBombs: function(){ var list = bombs.slice(); for (var i = 0; i \u003c list.length; i++) explodeBomb(list[i]); },\n spawnEnemyAt: function(x, y){ enemies.push({ x: x, y: y, alive: true, id: 999 + enemies.length }); },\n killPlayer: function(){ killPlayer(); },\n restartWithSeed: function(seed){ __seed = seed; window.__seed = seed; puzzleProgress = null; init(); },\n getDoorAt: function(x, y){ for (var d = 0; d \u003c doorMeta.length; d++) if (doorMeta[d].x === x && doorMeta[d].y === y) return d; return -1; },\n solveDoor: function(id){ onDoorSolved(id); },\n teleportPlayer: function(x, y){ player.x = x; player.y = y; },\n bfsStep: function(sx, sy, tx, ty){ return bfsStep(sx, sy, tx, ty); },\n setTile: function(x, y, t){ if (map && map[y]) map[y][x] = t; },\n getTile: function(x, y){ return map && map[y] ? map[y][x] : -1; }\n};\n\nvar mIdx = -1, mAtt = 0, mHint = false, mCb = null;\nel('mHintBtn').onclick = function(){ mHint = true; el('mHintText').style.display = 'block'; };\nel('mClose').onclick = function(){ el('mOverlay').style.display = 'none'; };\nfunction modalOpen(){ return el('mOverlay').style.display === 'flex'; }\nfunction openPuzzle(i, cb){\n mIdx = i; mAtt = 0; mHint = false; mCb = cb;\n var p = CFG.puzzles[i];\n el('mTitle').textContent = p.title || ('Puzzle ' + (i + 1));\n el('mQ').textContent = p.question;\n el('mFeedback').textContent = ''; el('mFeedback').className = 'mfb';\n el('mHintText').style.display = 'none'; el('mHintText').textContent = p.hint || '';\n el('mHintBtn').style.display = p.hint ? '' : 'none';\n var box = el('mAnswers'); box.innerHTML = '';\n if (p.type === 'free') {\n var inp = document.createElement('input'); inp.type = 'text'; inp.placeholder = 'Scrie raspunsul...'; inp.autocomplete = 'off';\n var b = document.createElement('button'); b.textContent = 'Verifica';\n b.onclick = function(){ mCheck(inp.value); };\n inp.onkeydown = function(e){ e.stopPropagation(); if (e.key === 'Enter') b.click(); };\n box.appendChild(inp); box.appendChild(b);\n setTimeout(function(){ inp.focus(); }, 60);\n } else if (p.type === 'tf') {\n ['Adevarat', 'Fals'].forEach(function(v){ var b = document.createElement('button'); b.className = 'opt'; b.textContent = v; b.onclick = function(){ mCheck(v); }; box.appendChild(b); });\n } else {\n choiceOpts(p).forEach(function(o){ var b = document.createElement('button'); b.className = 'opt'; b.textContent = o; b.onclick = function(){ mCheck(o); }; box.appendChild(b); });\n }\n el('mOverlay').style.display = 'flex';\n}\nfunction mCheck(given){\n var p = CFG.puzzles[mIdx];\n if (checkAnswer(p, given)) {\n var s = starsFor(mAtt, mHint);\n totalStars += s; beep(true);\n el('mFeedback').textContent = 'Corect! +' + s + ' \\u2605'; el('mFeedback').className = 'mfb good';\n setTimeout(function(){ el('mOverlay').style.display = 'none'; var cb = mCb; mCb = null; if (cb) cb(mIdx, s); }, 750);\n } else {\n mAtt++; beep(false);\n el('mFeedback').textContent = 'Nu e bine, mai incearca!'; el('mFeedback').className = 'mfb bad';\n var c = el('mCard'); c.classList.remove('shake'); void c.offsetWidth; c.classList.add('shake');\n }\n}\nfunction showFinal(){\n if(CFG._campaign){ campaignDone(); return; }\n el('fStars').textContent = totalStars + ' / ' + (CFG.puzzles.length * 3) + ' \\u2605';\n var w = finalWord(); var bw = el('fWord'); bw.innerHTML = '';\n for (var j = 0; j \u003c w.length; j++){ var s = document.createElement('span'); s.textContent = w.charAt(j); s.style.animationDelay = (j * 0.18) + 's'; bw.appendChild(s); }\n var msg = CFG.finalMessage || '';\n el('fMsg').textContent = CFG.player ? CFG.player + ', ' + msg.charAt(0).toLowerCase() + msg.slice(1) : msg;\n el('fOverlay').style.display = 'flex';\n beep(true); confetti();\n}\nel('fAgain').onclick = function(){ location.reload(); };\ninit();\nroomReady();\n\u003c/script>\n\u003c/body>\n\u003c/html>"}; var MASTER = {"title":"Comoara ascunsa","player":"","color":"#6d28d9","charName":"Alex","style":"campaign","story":"O comoara a fost ascunsa, iar singurul drum spre ea trece prin cateva incercari.","finalMessage":"Felicitari! Ai gasit comoara!","puzzles":[{"title":"Incalzirea","type":"free","question":"Cat fac 7 x 8?","answer":"56","tfAnswer":"Adevarat","choices":"","hint":"Tabla inmultirii cu 7.","letter":"D","style":""},{"title":"Adevarat sau fals","type":"tf","question":"Romania are iesire la Marea Neagra.","answer":"","tfAnswer":"Adevarat","choices":"","hint":"","letter":"A","style":""},{"title":"Alege raspunsul","type":"choice","question":"Care este capitala Frantei?","answer":"","tfAnswer":"Adevarat","choices":"*Paris\nLyon\nMarsilia","hint":"Turnul Eiffel.","letter":"R","style":""}]}; var ROTATION = ['classic','terminal','arcade','chat','point']; var TOKEN = '__CFG__'; diff --git a/exemplu-chat.html b/exemplu-chat.html index 10b0215..0d97313 100644 --- a/exemplu-chat.html +++ b/exemplu-chat.html @@ -74,6 +74,9 @@ function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type function beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} } function confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i < 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } } function roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } } +/* Contract de finalizare a camerei — un singur loc pentru payload-ul parent.nextRoom + (înlocuiește duplicatele din showFinal/finale; D7). Citește totalStars + finalWord() la apel. */ +function campaignDone(){ if(CFG._campaign){ try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:finalWord().charAt(0)}); }catch(e){} } } window.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } }; if(CFG._campaign){ /* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */ @@ -170,11 +173,7 @@ var chatIntro = CFG._campaign : ['Salut' + (CFG.player ? ', ' + CFG.player : '') + '!'].concat(storyChunks()).concat(['Ma ajuti sa ies de aici?']); seq(chatIntro, next); function showFinal(){ - if(CFG._campaign){ - var L = finalWord().charAt(0); - try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L}); }catch(e){} - return; - } + if(CFG._campaign){ campaignDone(); return; } el('fStars').textContent = totalStars + ' / ' + (CFG.puzzles.length * 3) + ' \u2605'; var w = finalWord(); var bw = el('fWord'); bw.innerHTML = ''; for (var j = 0; j < w.length; j++){ var s = document.createElement('span'); s.textContent = w.charAt(j); s.style.animationDelay = (j * 0.18) + 's'; bw.appendChild(s); } diff --git a/exemplu-point.html b/exemplu-point.html index 21eca13..6d3c09d 100644 --- a/exemplu-point.html +++ b/exemplu-point.html @@ -88,6 +88,9 @@ function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type function beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} } function confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i < 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } } function roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } } +/* Contract de finalizare a camerei — un singur loc pentru payload-ul parent.nextRoom + (înlocuiește duplicatele din showFinal/finale; D7). Citește totalStars + finalWord() la apel. */ +function campaignDone(){ if(CFG._campaign){ try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:finalWord().charAt(0)}); }catch(e){} } } window.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } }; if(CFG._campaign){ /* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */ @@ -202,11 +205,7 @@ function mCheck(given){ } } function showFinal(){ - if(CFG._campaign){ - var L = finalWord().charAt(0); - try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L}); }catch(e){} - return; - } + if(CFG._campaign){ campaignDone(); return; } el('fStars').textContent = totalStars + ' / ' + (CFG.puzzles.length * 3) + ' \u2605'; var w = finalWord(); var bw = el('fWord'); bw.innerHTML = ''; for (var j = 0; j < w.length; j++){ var s = document.createElement('span'); s.textContent = w.charAt(j); s.style.animationDelay = (j * 0.18) + 's'; bw.appendChild(s); } diff --git a/exemplu-terminal.html b/exemplu-terminal.html index 2c91311..9b82e21 100644 --- a/exemplu-terminal.html +++ b/exemplu-terminal.html @@ -41,6 +41,9 @@ function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type function beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} } function confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i < 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } } function roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } } +/* Contract de finalizare a camerei — un singur loc pentru payload-ul parent.nextRoom + (înlocuiește duplicatele din showFinal/finale; D7). Citește totalStars + finalWord() la apel. */ +function campaignDone(){ if(CFG._campaign){ try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:finalWord().charAt(0)}); }catch(e){} } } window.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } }; if(CFG._campaign){ /* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */ @@ -98,9 +101,7 @@ function finale(){ done = true; if(CFG._campaign){ var s = totalStars; var L = finalWord().charAt(0); - say(['>> CAMERA REZOLVATA! Stele: ' + s + (L ? ' | Litera: ' + L : '')], 'ok', function(){ - try{ parent.nextRoom({idx:CFG._campaign.idx, stars:s, letter:L}); }catch(e){} - }); + say(['>> CAMERA REZOLVATA! Stele: ' + s + (L ? ' | Litera: ' + L : '')], 'ok', campaignDone); return; } var w = finalWord().split('').join(' ');