Files
escape-builder/escape-builder.html
Claude Agent d67f6ddc15 S3 pas 1: Bomberman complet în gameArcade
Înlocuiește labirintul simplu cu Bomberman (port din scratch/bomberman-proto.html):
bombe + explozii în lanț, cutii distructibile, AI dușmani BFS urmărire, 3 vieți +
respawn cu progres puzzle păstrat, plasare aleatoare (PRNG seedat), buton bombă +
overlay game-over. Păstrează contractul motorului: openPuzzle/onDoorSolved/showFinal/
modalOpen/roomReady — uși=N puzzle-uri (modal real), cufăr=scăpare. Demo regenerat.

Verificat: smoke 21/21 (zero regresie) + gameplay 6/6 in arcade-ul integrat
(bombă sparge cutie, AI urmărește, respawn păstrează progres, ușă→modal real,
cufăr→final) + captură vizuală. Board: TODOS.md S3 pas 1 [x].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 10:25:48 +00:00

2095 lines
107 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Escape Room Builder</title>
<style>
:root {
--bg: #f4f5f7; --panel: #ffffff; --ink: #1f2430; --muted: #6b7280;
--accent: #6d28d9; --accent-soft: #ede9fe; --line: #e5e7eb; --danger: #dc2626;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
background: var(--bg); color: var(--ink); display: flex; flex-direction: column;
}
header {
display: flex; align-items: center; gap: 12px; padding: 10px 16px;
background: var(--panel); border-bottom: 1px solid var(--line);
}
header h1 { font-size: 16px; margin: 0; font-weight: 700; }
header .spacer { flex: 1; }
button {
font: inherit; cursor: pointer; border-radius: 8px; border: 1px solid var(--line);
background: var(--panel); color: var(--ink); padding: 7px 14px;
}
button:hover { border-color: var(--accent); color: var(--accent); }
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
button.primary:hover { filter: brightness(1.1); color: #fff; }
button.ghost { border: none; background: none; padding: 4px 6px; color: var(--muted); }
button.ghost:hover { color: var(--accent); }
button.ghost.del:hover { color: var(--danger); }
main { flex: 1; display: flex; min-height: 0; }
#editor {
width: 460px; min-width: 380px; overflow-y: auto; padding: 16px;
border-right: 1px solid var(--line); background: var(--bg);
}
#previewPane { flex: 1; display: flex; flex-direction: column; min-width: 0; }
#previewPane .bar {
padding: 6px 12px; font-size: 12px; color: var(--muted);
background: var(--panel); border-bottom: 1px solid var(--line);
display: flex; align-items: center; gap: 10px;
}
iframe { flex: 1; border: 0; width: 100%; background: #1a1033; }
fieldset {
border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
padding: 12px 14px; margin: 0 0 14px;
}
legend { font-size: 12px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; padding: 0 6px; }
label { display: block; font-size: 12px; font-weight: 600; color: var(--muted); margin: 10px 0 3px; }
label:first-of-type { margin-top: 0; }
input[type=text], textarea, select {
width: 100%; font: inherit; font-size: 14px; padding: 7px 9px;
border: 1px solid var(--line); border-radius: 7px; background: #fff; color: var(--ink);
}
input:focus, textarea:focus, select:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
textarea { resize: vertical; min-height: 54px; }
.row { display: flex; gap: 10px; }
.row > div { flex: 1; }
.row > div.narrow { flex: 0 0 110px; }
.puzzle {
border: 1px solid var(--line); border-radius: 10px; background: var(--panel); margin-bottom: 10px;
}
.puzzle > .head {
display: flex; align-items: center; gap: 6px; padding: 8px 10px; cursor: pointer; user-select: none;
}
.puzzle > .head .num {
background: var(--accent-soft); color: var(--accent); font-weight: 700; font-size: 12px;
border-radius: 6px; padding: 2px 8px;
}
.puzzle > .head .t { flex: 1; font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.puzzle > .body { padding: 0 12px 12px; border-top: 1px solid var(--line); }
.puzzle.closed > .body { display: none; }
.word {
font-family: ui-monospace, monospace; font-size: 18px; letter-spacing: 6px;
background: var(--accent-soft); color: var(--accent); border-radius: 7px;
padding: 6px 10px; display: inline-block; min-height: 32px;
}
.help { font-size: 11px; color: var(--muted); margin-top: 3px; }
#addPuzzle { width: 100%; padding: 10px; border-style: dashed; color: var(--muted); }
#addPuzzle:hover { color: var(--accent); }
input[type=color] { border: 1px solid var(--line); border-radius: 7px; height: 34px; width: 100%; padding: 2px; background: #fff; }
</style>
</head>
<body>
<header>
<h1>Escape Room Builder</h1>
<div class="spacer"></div>
<button id="btnNew">Proiect nou</button>
<button id="btnLoad">Incarca JSON</button>
<button id="btnSaveJson">Salveaza JSON</button>
<button id="btnExport" class="primary">Exporta jocul HTML</button>
<input type="file" id="fileLoad" accept=".json" hidden>
</header>
<main>
<section id="editor">
<fieldset>
<legend>Joc</legend>
<div class="row">
<div>
<label>Titlul jocului</label>
<input type="text" id="gTitle" data-g="title">
</div>
<div class="narrow">
<label>Culoare</label>
<input type="color" id="gColor" data-g="color">
</div>
</div>
<div class="row">
<div>
<label>Stil joc</label>
<select id="gStyle" data-g="style">
<option value="classic">Clasic (quiz)</option>
<option value="terminal">Terminal retro</option>
<option value="arcade">Arcade pixel</option>
<option value="chat">Story chat</option>
<option value="point">Point-and-click</option>
<option value="campaign">Campanie multi-stil</option>
</select>
</div>
<div>
<label>Personaj (Story chat)</label>
<input type="text" id="gChar" data-g="charName" placeholder="Alex">
</div>
</div>
<label>Pentru cine (optional, apare in mesaje)</label>
<input type="text" id="gPlayer" data-g="player" placeholder="ex: Paula">
<label>Povestea de inceput</label>
<textarea id="gStory" data-g="story" rows="3"></textarea>
<label>Mesajul final (la castig)</label>
<textarea id="gFinal" data-g="finalMessage" rows="2"></textarea>
<label>Cuvantul final (din literele puzzle-urilor)</label>
<div class="word" id="finalWord">&nbsp;</div>
<div class="help">Se compune automat din campul "Litera" al fiecarui puzzle, in ordine.</div>
</fieldset>
<fieldset>
<legend>Puzzle-uri</legend>
<div id="puzzleList"></div>
<button id="addPuzzle">+ Adauga puzzle</button>
</fieldset>
</section>
<section id="previewPane">
<div class="bar">
<span>Preview live - jocul e jucabil aici, exact cum va arata exportat</span>
<div class="spacer" style="flex:1"></div>
<button id="btnReload" class="ghost">Reporneste jocul</button>
</div>
<iframe id="frame" title="Preview joc"></iframe>
</section>
</main>
<script>
'use strict';
/* ---------- stare ---------- */
const STORAGE_KEY = 'escape-builder-v1';
const CAMPAIGN_ROTATION = ['classic', 'terminal', 'arcade', 'chat', 'point'];
const CAMPAIGN_STYLE_NAMES = { classic: 'Clasic', terminal: 'Terminal Retro', arcade: 'Arcade Pixel', chat: 'Story Chat', point: 'Point-and-Click' };
const defaultState = () => ({
title: 'Comoara ascunsa',
player: '',
color: '#6d28d9',
style: 'classic',
charName: 'Alex',
story: 'O comoara a fost ascunsa, iar singurul drum spre ea trece prin cateva incercari. Rezolva fiecare puzzle ca sa aduni literele cuvantului magic.',
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' },
{ title: 'Adevarat sau fals', type: 'tf', question: 'Romania are iesire la Marea Neagra.', answer: '', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'A' },
{ title: 'Alege raspunsul', type: 'choice', question: 'Care este capitala Frantei?', answer: '', tfAnswer: 'Adevarat', choices: '*Paris\nLyon\nMarsilia', hint: 'Turnul Eiffel.', letter: 'R' }
]
});
function normalizePuzzle(p) {
const validTypes = ['free', 'tf', 'choice'];
const validStyles = ['', 'classic', 'terminal', 'arcade', 'chat', 'point'];
if (typeof p.title !== 'string') p.title = '';
if (!validTypes.includes(p.type)) p.type = 'free';
if (typeof p.question !== 'string') p.question = '';
if (typeof p.answer !== 'string') p.answer = '';
if (p.tfAnswer !== 'Adevarat' && p.tfAnswer !== 'Fals') p.tfAnswer = 'Adevarat';
if (typeof p.choices !== 'string') p.choices = '';
if (typeof p.hint !== 'string') p.hint = '';
if (typeof p.letter !== 'string') p.letter = '';
if (!validStyles.includes(p.style || '')) p.style = '';
if (typeof p.style === 'undefined') p.style = '';
return p;
}
const blankPuzzle = () => normalizePuzzle({ title: '', type: 'free', question: '', answer: '', tfAnswer: 'Adevarat', choices: '', hint: '', letter: '', style: '' });
let state = Object.assign(defaultState(), load() || {});
if (Array.isArray(state.puzzles)) state.puzzles = state.puzzles.map(normalizePuzzle);
function load() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)); } catch (e) { return null; }
}
function persist() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) { /* quota/private mode — continuă fără autosave (D12) */ }
}
/* ---------- editor ---------- */
const $ = sel => document.querySelector(sel);
const puzzleList = $('#puzzleList');
function renderGlobals() {
document.querySelectorAll('[data-g]').forEach(el => { el.value = state[el.dataset.g]; });
renderWord();
}
function renderWord() {
const word = state.puzzles.map(p => (p.letter || '').trim().charAt(0).toUpperCase()).join('');
$('#finalWord').textContent = word || ' ';
}
function puzzleCard(p, i) {
const div = document.createElement('div');
div.className = 'puzzle' + (p._closed ? ' closed' : '');
div.dataset.i = i;
div.innerHTML = `
<div class="head">
<span class="num">${i + 1}</span>
<span class="t">${esc(p.title || p.question || 'Puzzle fara titlu')}</span>
<button class="ghost" data-act="up" title="Muta sus">&#9650;</button>
<button class="ghost" data-act="down" title="Muta jos">&#9660;</button>
<button class="ghost del" data-act="del" title="Sterge">&#10005;</button>
</div>
<div class="body">
<div class="row">
<div>
<label>Titlu scurt</label>
<input type="text" data-f="title" value="${esc(p.title)}">
</div>
<div class="narrow">
<label>Tip</label>
<select data-f="type">
<option value="free" ${p.type === 'free' ? 'selected' : ''}>Raspuns liber</option>
<option value="tf" ${p.type === 'tf' ? 'selected' : ''}>Adevarat / Fals</option>
<option value="choice" ${p.type === 'choice' ? 'selected' : ''}>Variante</option>
</select>
</div>
${state.style === 'campaign' ? `
<div class="narrow">
<label>Stil cameră</label>
<select data-f="style">
<option value="" ${!p.style ? 'selected' : ''}>Auto (${esc(CAMPAIGN_STYLE_NAMES[CAMPAIGN_ROTATION[i % CAMPAIGN_ROTATION.length]])})</option>
<option value="classic" ${p.style === 'classic' ? 'selected' : ''}>Clasic</option>
<option value="terminal" ${p.style === 'terminal' ? 'selected' : ''}>Terminal Retro</option>
<option value="arcade" ${p.style === 'arcade' ? 'selected' : ''}>Arcade Pixel</option>
<option value="chat" ${p.style === 'chat' ? 'selected' : ''}>Story Chat</option>
<option value="point" ${p.style === 'point' ? 'selected' : ''}>Point-and-Click</option>
</select>
</div>` : ''}
</div>
${state.style === 'campaign' && p.style && !CAMPAIGN_STYLE_NAMES[p.style] ? `<div class="help" style="color:#f87171">Stil necunoscut "${esc(p.style)}" — se foloseste Auto</div>` : ''}
<label>Intrebarea</label>
<textarea data-f="question" rows="2">${esc(p.question)}</textarea>
${p.type === 'free' ? `
<label>Raspunsul corect</label>
<input type="text" data-f="answer" value="${esc(p.answer)}">
<div class="help">Nu conteaza literele mari/mici sau diacriticele.</div>` : ''}
${p.type === 'tf' ? `
<label>Raspunsul corect</label>
<select data-f="tfAnswer">
<option ${p.tfAnswer === 'Adevarat' ? 'selected' : ''}>Adevarat</option>
<option ${p.tfAnswer === 'Fals' ? 'selected' : ''}>Fals</option>
</select>` : ''}
${p.type === 'choice' ? `
<label>Variante (una pe linie, pune * inaintea celei corecte)</label>
<textarea data-f="choices" rows="3">${esc(p.choices)}</textarea>` : ''}
<div class="row">
<div>
<label>Indiciu (optional)</label>
<input type="text" data-f="hint" value="${esc(p.hint)}">
</div>
<div class="narrow">
<label>Litera</label>
<input type="text" data-f="letter" maxlength="1" value="${esc(p.letter)}">
</div>
</div>
</div>`;
return div;
}
function renderPuzzles() {
puzzleList.innerHTML = '';
state.puzzles.forEach((p, i) => puzzleList.appendChild(puzzleCard(p, i)));
}
function esc(s) {
return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* evenimente editor */
document.querySelectorAll('[data-g]').forEach(el => {
el.addEventListener('input', () => {
state[el.dataset.g] = el.value;
if (el.dataset.g === 'style') renderPuzzles(); /* re-render: style selector per card apare/dispare */
onChange();
});
});
puzzleList.addEventListener('input', e => {
const f = e.target.dataset.f;
if (!f) return;
const i = +e.target.closest('.puzzle').dataset.i;
state.puzzles[i][f] = e.target.value;
if (f === 'title' || f === 'question') {
const card = e.target.closest('.puzzle');
card.querySelector('.head .t').textContent = state.puzzles[i].title || state.puzzles[i].question || 'Puzzle fara titlu';
}
if (f === 'type') { state.puzzles[i]._closed = false; renderPuzzles(); }
onChange();
});
puzzleList.addEventListener('click', e => {
const btn = e.target.closest('button[data-act]');
if (btn) {
const card = btn.closest('.puzzle');
const i = +card.dataset.i;
const act = btn.dataset.act;
if (act === 'del') {
if (!confirm('Stergi puzzle-ul ' + (i + 1) + '?')) return;
state.puzzles.splice(i, 1);
}
if (act === 'up' && i > 0) [state.puzzles[i - 1], state.puzzles[i]] = [state.puzzles[i], state.puzzles[i - 1]];
if (act === 'down' && i < state.puzzles.length - 1) [state.puzzles[i + 1], state.puzzles[i]] = [state.puzzles[i], state.puzzles[i + 1]];
renderPuzzles(); onChange();
return;
}
const head = e.target.closest('.head');
if (head && !e.target.closest('input,select,textarea')) {
const card = head.closest('.puzzle');
const i = +card.dataset.i;
state.puzzles[i]._closed = !state.puzzles[i]._closed;
card.classList.toggle('closed');
}
});
$('#addPuzzle').addEventListener('click', () => {
state.puzzles.forEach(p => p._closed = true);
state.puzzles.push(blankPuzzle());
renderPuzzles(); onChange();
puzzleList.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
puzzleList.lastElementChild.querySelector('[data-f=title]').focus();
});
$('#btnNew').addEventListener('click', () => {
if (!confirm('Incepi un proiect nou? Proiectul curent se pierde (salveaza-l mai intai ca JSON daca vrei sa-l pastrezi).')) return;
state = defaultState();
renderGlobals(); renderPuzzles(); onChange();
});
$('#btnSaveJson').addEventListener('click', () => {
download(slug(state.title) + '.json', JSON.stringify(cleanState(), null, 2), 'application/json');
});
$('#btnLoad').addEventListener('click', () => $('#fileLoad').click());
$('#fileLoad').addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
file.text().then(txt => {
try {
const data = JSON.parse(txt);
if (!Array.isArray(data.puzzles)) throw new Error('format');
state = Object.assign(defaultState(), data);
state.puzzles = state.puzzles.map(normalizePuzzle);
renderGlobals(); renderPuzzles(); onChange();
} catch (err) {
alert('Fisierul nu este un proiect valid de escape room.');
}
});
e.target.value = '';
});
$('#btnExport').addEventListener('click', () => {
if (state.puzzles.length === 0) { alert('Adauga cel putin un puzzle inainte de export!'); return; }
download(slug(state.title) + '.html', gameHTML(cleanState()), 'text/html');
});
$('#btnReload').addEventListener('click', refreshPreview);
function cleanState() {
const s = JSON.parse(JSON.stringify(state));
s.puzzles.forEach(p => {
delete p._closed;
/* D13: letter normalizat la 1 caracter alfanumeric (bug: un < strică scena SVG din point) */
p.letter = (p.letter || '').replace(/[^A-Za-z0-9]/g, '').charAt(0);
});
return s;
}
function slug(s) {
return (s || 'escape-room').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'escape-room';
}
function download(name, content, mime) {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([content], { type: mime }));
a.download = name;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
}
/* ---------- preview ---------- */
let timer = null;
function onChange() {
persist(); renderWord();
clearTimeout(timer);
timer = setTimeout(refreshPreview, 400);
}
function refreshPreview() {
if (state.puzzles.length === 0) {
$('#frame').srcdoc = '<html><body style="font:system-ui,sans-serif;color:#fff;background:#0d0620;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;text-align:center;padding:20px"><div><div style="font-size:48px;margin-bottom:16px">🚪</div><p style="opacity:.7;line-height:1.6;margin:0">Adaugă cel puțin un puzzle<br>ca să vezi preview-ul.</p></div></body></html>';
return;
}
const previewCfg = cleanState();
if (previewCfg.style === 'campaign') previewCfg._noResume = true; /* preview nu reia niciodată (D3) */
$('#frame').srcdoc = gameHTML(previewCfg);
}
/* ---------- template-urile jocului exportat ---------- */
function gameHTML(cfg) {
const engines = { classic: gameClassic, terminal: gameTerminal, arcade: gameArcade, chat: gameChat, point: gamePoint, campaign: gameCampaign };
return (engines[cfg.style] || gameClassic)(cfg);
}
function gameClassic(cfg) {
/* cfg === '__TEMPLATE__' → emit sentinel __CFG__ în loc de JSON (D1) */
const json = (cfg === '__TEMPLATE__') ? '__CFG__' : JSON.stringify(cfg).replace(/</g, '\\u003c');
return `<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(cfg.title)}</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
color: #fff; display: flex; align-items: center; justify-content: center; padding: 16px;
background: linear-gradient(160deg, #14092e 0%, #2a1257 55%, #14092e 100%);
}
.card {
width: 100%; max-width: 560px; background: rgba(255,255,255,.07);
border: 1px solid rgba(255,255,255,.14); border-radius: 18px; padding: 26px;
backdrop-filter: blur(6px); box-shadow: 0 18px 50px rgba(0,0,0,.45);
}
h1 { margin: 0 0 6px; font-size: 26px; text-align: center; }
.story { color: rgba(255,255,255,.8); text-align: center; line-height: 1.5; }
.screen { display: none; }
.screen.on { display: block; animation: pop .35s ease; }
@keyframes pop { from { transform: scale(.96); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.progress { height: 7px; background: rgba(255,255,255,.15); border-radius: 99px; overflow: hidden; margin: 14px 0 4px; }
.progress i { display: block; height: 100%; background: var(--accent); width: 0; transition: width .4s ease; }
.meta { display: flex; justify-content: space-between; font-size: 12px; color: rgba(255,255,255,.6); margin-bottom: 14px; }
.letters { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; margin: 14px 0; }
.tile {
width: 34px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center;
font-weight: 800; font-size: 18px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.18);
color: rgba(255,255,255,.35);
}
.tile.won { background: var(--accent); color: #fff; border-color: transparent; animation: flip .5s ease; }
@keyframes flip { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
.qtitle { font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--accent-light); font-weight: 700; }
.question { font-size: 19px; line-height: 1.45; margin: 8px 0 18px; }
input[type=text] {
width: 100%; font: inherit; font-size: 18px; padding: 11px 13px; border-radius: 10px;
border: 1px solid rgba(255,255,255,.25); background: rgba(0,0,0,.25); color: #fff; text-align: center;
}
input:focus { outline: 2px solid var(--accent); border-color: transparent; }
button {
font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px;
font-weight: 700; background: var(--accent); color: #fff; width: 100%; margin-top: 10px;
}
button:hover { filter: brightness(1.12); }
button.opt { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); font-weight: 600; text-align: left; }
button.opt:hover { background: rgba(255,255,255,.18); }
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; }
button.hint:hover { color: #fff; }
.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; }
.feedback { min-height: 22px; text-align: center; font-weight: 700; margin-top: 10px; }
.feedback.bad { color: #fda4af; }
.feedback.good { color: #86efac; }
.shake { animation: shake .4s ease; }
@keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }
.stars { text-align: center; font-size: 26px; letter-spacing: 4px; color: #fbbf24; margin: 6px 0; }
.bigword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 18px 0; }
.bigword 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: flip .6s ease backwards;
}
.confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 5; animation: fall linear forwards; }
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
</style>
</head>
<body>
<div class="card">
<div id="sStart" class="screen on">
<h1 id="gtitle"></h1>
<p class="story" id="gstory"></p>
<button id="btnStart">Incepe aventura</button>
</div>
<div id="sGame" class="screen">
<div class="progress"><i id="bar"></i></div>
<div class="meta"><span id="step"></span><span id="score"></span></div>
<div class="letters" id="lettersBar"></div>
<div id="qbox">
<div class="qtitle" id="qtitle"></div>
<div class="question" id="qtext"></div>
<div id="answers"></div>
<div class="feedback" id="feedback"></div>
<button class="hint" id="btnHint">Vreau un indiciu</button>
<div class="hinttext" id="hinttext"></div>
</div>
</div>
<div id="sFinal" class="screen">
<h1>Evadare reusita!</h1>
<div class="stars" id="finalStars"></div>
<div class="bigword" id="bigword"></div>
<p class="story" id="finalMsg"></p>
<button id="btnAgain">Joaca din nou</button>
</div>
</div>
<script>
var CFG = ${json};
document.documentElement.style.setProperty('--accent', CFG.color || '#6d28d9');
document.documentElement.style.setProperty('--accent-light', 'color-mix(in srgb, ' + (CFG.color || '#6d28d9') + ' 40%, white)');
var idx = 0, totalStars = 0, attempts = 0, hintUsed = false, won = [];
function el(id) { return document.getElementById(id); }
function norm(s) {
return String(s).trim().toLowerCase().normalize('NFD')
.replace(/[\\u0300-\\u036f]/g, '').replace(/\\s+/g, ' ').replace(/,/g, '.');
}
function show(id) {
var scr = document.querySelectorAll('.screen');
for (var i = 0; i < scr.length; i++) scr[i].classList.remove('on');
el(id).classList.add('on');
}
el('gtitle').textContent = CFG.title;
var hello = CFG.player ? 'Salut, ' + CFG.player + '! ' : '';
el('gstory').textContent = hello + CFG.story;
el('btnStart').onclick = function () { show('sGame'); renderPuzzle(); };
el('btnAgain').onclick = function () { location.reload(); };
function lettersBar() {
var bar = el('lettersBar');
bar.innerHTML = '';
var any = false;
for (var i = 0; i < CFG.puzzles.length; i++) {
var L = (CFG.puzzles[i].letter || '').trim();
if (!L) continue;
any = true;
var d = document.createElement('div');
d.className = 'tile' + (won[i] ? ' won' : '');
d.textContent = won[i] ? L.toUpperCase() : '?';
bar.appendChild(d);
}
bar.style.display = any ? '' : 'none';
}
function renderPuzzle() {
var p = CFG.puzzles[idx];
attempts = 0; hintUsed = false;
el('bar').style.width = (idx / CFG.puzzles.length * 100) + '%';
el('step').textContent = 'Puzzle ' + (idx + 1) + ' din ' + CFG.puzzles.length;
el('score').textContent = totalStars + ' \\u2605';
el('qtitle').textContent = p.title || 'Puzzle ' + (idx + 1);
el('qtext').textContent = p.question;
el('feedback').textContent = ''; el('feedback').className = 'feedback';
el('hinttext').style.display = 'none';
el('hinttext').textContent = p.hint || '';
el('btnHint').style.display = p.hint ? '' : 'none';
lettersBar();
var box = el('answers');
box.innerHTML = '';
if (p.type === 'free') {
var inp = document.createElement('input');
inp.type = 'text'; inp.autocomplete = 'off'; inp.placeholder = 'Scrie raspunsul...';
var btn = document.createElement('button');
btn.textContent = 'Verifica';
btn.onclick = function () { check(inp.value, p.answer); };
inp.onkeydown = function (e) { if (e.key === 'Enter') btn.click(); };
box.appendChild(inp); box.appendChild(btn);
setTimeout(function () { inp.focus(); }, 50);
} else if (p.type === 'tf') {
['Adevarat', 'Fals'].forEach(function (v) {
var b = document.createElement('button');
b.className = 'opt'; b.textContent = v;
b.onclick = function () { check(v, p.tfAnswer); };
box.appendChild(b);
});
} else {
var correct = '';
var opts = (p.choices || '').split('\\n').map(function (l) { return l.trim(); }).filter(Boolean);
opts.forEach(function (o) { if (o.charAt(0) === '*') correct = o.slice(1).trim(); });
opts.map(function (o) { return o.charAt(0) === '*' ? o.slice(1).trim() : o; })
.forEach(function (o) {
var b = document.createElement('button');
b.className = 'opt'; b.textContent = o;
b.onclick = function () { check(o, correct); };
box.appendChild(b);
});
if (!opts.length) box.textContent = '(puzzle fara variante - completeaza-le in builder)';
}
}
el('btnHint').onclick = function () {
hintUsed = true;
el('hinttext').style.display = 'block';
};
function check(given, expected) {
if (norm(given) === norm(expected) && norm(given) !== '') {
var stars = (hintUsed || attempts >= 2) ? 1 : (attempts === 1 ? 2 : 3);
totalStars += stars;
won[idx] = true;
beep(true);
var f = el('feedback');
f.textContent = 'Corect! +' + stars + ' \\u2605';
f.className = 'feedback good';
lettersBar();
el('bar').style.width = ((idx + 1) / CFG.puzzles.length * 100) + '%';
setTimeout(next, 900);
} else {
attempts++;
beep(false);
var fb = el('feedback');
fb.textContent = 'Nu e bine, mai incearca!';
fb.className = 'feedback bad';
var card = document.querySelector('.card');
card.classList.remove('shake');
void card.offsetWidth;
card.classList.add('shake');
}
}
function next() {
idx++;
if (idx < CFG.puzzles.length) { renderPuzzle(); return; }
if(CFG._campaign){
var L = ''; for(var ci=0;ci<CFG.puzzles.length;ci++){var lc=(CFG.puzzles[ci].letter||'').trim();if(lc)L+=lc.toUpperCase();}
try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L.charAt(0)}); }catch(e){}
return;
}
show('sFinal');
var max = CFG.puzzles.length * 3;
el('finalStars').textContent = totalStars + ' / ' + max + ' \\u2605';
var word = '';
for (var i = 0; i < CFG.puzzles.length; i++) {
var L = (CFG.puzzles[i].letter || '').trim();
if (L) word += L.toUpperCase();
}
var bw = el('bigword');
bw.innerHTML = '';
for (var j = 0; j < word.length; j++) {
var s = document.createElement('span');
s.textContent = word.charAt(j);
s.style.animationDelay = (j * 0.18) + 's';
bw.appendChild(s);
}
var name = CFG.player ? CFG.player + ', ' : '';
el('finalMsg').textContent = name ? name + (CFG.finalMessage || '').charAt(0).toLowerCase() + (CFG.finalMessage || '').slice(1) : (CFG.finalMessage || '');
confetti();
}
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 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 freqs = ok ? [523, 784] : [196];
freqs.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) {}
}
window.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } };
if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} }
<\/script>
</body>
</html>`;
}
/* ---------- biblioteca comuna pentru motoarele de joc ---------- */
function libJS(cfg) {
/* cfg === '__TEMPLATE__' → emit sentinel __CFG__ în loc de JSON real (D1) */
const json = (cfg === '__TEMPLATE__') ? '__CFG__' : JSON.stringify(cfg).replace(/</g, '\\u003c');
return `var CFG = ${json};
document.documentElement.style.setProperty('--accent', CFG.color || '#6d28d9');
var totalStars = 0;
function el(id){ return document.getElementById(id); }
function norm(s){ return String(s).trim().toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').replace(/\\s+/g, ' ').replace(/,/g, '.'); }
function starsFor(att, hint){ return (hint || att >= 2) ? 1 : (att === 1 ? 2 : 3); }
function finalWord(){ var w = ''; for (var i = 0; i < CFG.puzzles.length; i++){ var L = (CFG.puzzles[i].letter || '').trim(); if (L) w += L.toUpperCase(); } return w; }
function 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; }); }
function choiceCorrect(p){ var ls = (p.choices || '').split('\\n'); for (var i = 0; i < ls.length; i++){ var l = ls[i].trim(); if (l.charAt(0) === '*') return l.slice(1).trim(); } return ''; }
function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type === 'choice' ? choiceCorrect(p) : p.answer); return norm(given) !== '' && norm(given) === norm(exp); }
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){} } }
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 */
var _cs = document.createElement('style');
_cs.textContent = 'h1{display:none!important}.progress{display:none!important}.meta{display:none!important}';
(document.head || document.documentElement).appendChild(_cs);
}`;
}
const SNIP = {};
SNIP.baseCss = `
.confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 99; animation: fall linear forwards; }
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
.shake { animation: shake .4s ease; }
@keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }`;
SNIP.modalCss = `
#mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; }
#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); }
#mCard .mtitle { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #c4b5fd; font-weight: 700; }
#mCard .mq { font-size: 18px; line-height: 1.45; margin: 8px 0 16px; }
#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; }
#mCard input:focus { outline: 2px solid var(--accent); border-color: transparent; }
#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; }
#mCard button.opt { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); font-weight: 600; text-align: left; }
#mCard button.opt:hover { background: rgba(255,255,255,.2); }
#mCard .mfb { min-height: 20px; text-align: center; font-weight: 700; margin-top: 10px; }
#mCard .mfb.bad { color: #fda4af; } #mCard .mfb.good { color: #86efac; }
#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; }
#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; }
#mCard .mclose { background: none !important; color: rgba(255,255,255,.4) !important; font-size: 12px; width: auto !important; margin: 6px auto 0; display: block; }`;
SNIP.modalHtml = `<div id="mOverlay"><div id="mCard">
<div class="mtitle" id="mTitle"></div>
<div class="mq" id="mQ"></div>
<div id="mAnswers"></div>
<div class="mfb" id="mFeedback"></div>
<button class="mhint" id="mHintBtn">Vreau un indiciu</button>
<div class="mhinttext" id="mHintText"></div>
<button class="mclose" id="mClose">Pleaca de aici</button>
</div></div>`;
SNIP.modalJs = `var mIdx = -1, mAtt = 0, mHint = false, mCb = null;
el('mHintBtn').onclick = function(){ mHint = true; el('mHintText').style.display = 'block'; };
el('mClose').onclick = function(){ el('mOverlay').style.display = 'none'; };
function modalOpen(){ return el('mOverlay').style.display === 'flex'; }
function openPuzzle(i, cb){
mIdx = i; mAtt = 0; mHint = false; mCb = cb;
var p = CFG.puzzles[i];
el('mTitle').textContent = p.title || ('Puzzle ' + (i + 1));
el('mQ').textContent = p.question;
el('mFeedback').textContent = ''; el('mFeedback').className = 'mfb';
el('mHintText').style.display = 'none'; el('mHintText').textContent = p.hint || '';
el('mHintBtn').style.display = p.hint ? '' : 'none';
var box = el('mAnswers'); box.innerHTML = '';
if (p.type === 'free') {
var inp = document.createElement('input'); inp.type = 'text'; inp.placeholder = 'Scrie raspunsul...'; inp.autocomplete = 'off';
var b = document.createElement('button'); b.textContent = 'Verifica';
b.onclick = function(){ mCheck(inp.value); };
inp.onkeydown = function(e){ e.stopPropagation(); if (e.key === 'Enter') b.click(); };
box.appendChild(inp); box.appendChild(b);
setTimeout(function(){ inp.focus(); }, 60);
} else if (p.type === 'tf') {
['Adevarat', 'Fals'].forEach(function(v){ var b = document.createElement('button'); b.className = 'opt'; b.textContent = v; b.onclick = function(){ mCheck(v); }; box.appendChild(b); });
} else {
choiceOpts(p).forEach(function(o){ var b = document.createElement('button'); b.className = 'opt'; b.textContent = o; b.onclick = function(){ mCheck(o); }; box.appendChild(b); });
}
el('mOverlay').style.display = 'flex';
}
function mCheck(given){
var p = CFG.puzzles[mIdx];
if (checkAnswer(p, given)) {
var s = starsFor(mAtt, mHint);
totalStars += s; beep(true);
el('mFeedback').textContent = 'Corect! +' + s + ' \\u2605'; el('mFeedback').className = 'mfb good';
setTimeout(function(){ el('mOverlay').style.display = 'none'; var cb = mCb; mCb = null; if (cb) cb(mIdx, s); }, 750);
} else {
mAtt++; beep(false);
el('mFeedback').textContent = 'Nu e bine, mai incearca!'; el('mFeedback').className = 'mfb bad';
var c = el('mCard'); c.classList.remove('shake'); void c.offsetWidth; c.classList.add('shake');
}
}`;
SNIP.finalCss = `
#fOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.88); z-index: 30; align-items: center; justify-content: center; padding: 16px; }
#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; }
#fOverlay h1 { margin: 0 0 8px; font-size: 26px; }
#fOverlay .fstars { font-size: 26px; letter-spacing: 4px; color: #fbbf24; margin: 6px 0; }
#fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; }
#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; }
@keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
#fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; }
#fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }`;
SNIP.finalHtml = `<div id="fOverlay"><div class="fcard">
<h1>Evadare reusita!</h1>
<div class="fstars" id="fStars"></div>
<div class="fword" id="fWord"></div>
<p id="fMsg"></p>
<button id="fAgain">Joaca din nou</button>
</div></div>`;
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;
}
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); }
var msg = CFG.finalMessage || '';
el('fMsg').textContent = CFG.player ? CFG.player + ', ' + msg.charAt(0).toLowerCase() + msg.slice(1) : msg;
el('fOverlay').style.display = 'flex';
beep(true); confetti();
}
el('fAgain').onclick = function(){ location.reload(); };`;
/* ---------- motor: terminal retro ---------- */
function gameTerminal(cfg) {
return `<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(cfg.title)}</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: #04130a; color: #39ff6e; font-family: "Courier New", ui-monospace, monospace; }
#crt { max-width: 760px; margin: 0 auto; padding: 20px 16px 80px; }
.line { white-space: pre-wrap; word-break: break-word; line-height: 1.45; font-size: 15px; text-shadow: 0 0 7px rgba(57,255,110,.5); }
.line.dim { color: #1f9c4a; }
.line.warn { color: #ffd24a; text-shadow: 0 0 7px rgba(255,210,74,.45); }
.line.bad { color: #ff6b6b; text-shadow: 0 0 7px rgba(255,107,107,.45); }
.line.ok { color: #9dffc0; }
#inline { display: flex; gap: 8px; align-items: baseline; font-size: 15px; text-shadow: 0 0 7px rgba(57,255,110,.5); }
#cmd { flex: 1; background: none; border: none; outline: none; color: inherit; font: inherit; text-shadow: inherit; caret-color: #39ff6e; }
.scan { position: fixed; inset: 0; pointer-events: none; background: repeating-linear-gradient(0deg, rgba(0,0,0,.28) 0 1px, transparent 1px 3px); }
.vign { position: fixed; inset: 0; pointer-events: none; background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.6)); }
</style>
</head>
<body>
<div class="scan"></div><div class="vign"></div>
<div id="crt"><div id="out"></div>
<div id="inline"><span>&gt;</span><input id="cmd" autocomplete="off" autofocus spellcheck="false"></div>
</div>
<script>
${libJS(cfg)}
var idx = -1, attempts = 0, hintUsed = false, done = false;
var solved = [];
var out = el('out'), cmd = el('cmd');
document.body.addEventListener('click', function(){ cmd.focus(); });
var queue = [], typing = false;
function say(lines, cls, cb){ queue.push({ lines: lines.slice(), cls: cls || '', cb: cb }); pump(); }
function pump(){
if (typing) return;
var job = queue[0];
if (!job) return;
if (!job.lines.length) { queue.shift(); if (job.cb) job.cb(); pump(); return; }
var text = job.lines.shift();
typing = true;
var d = document.createElement('div');
d.className = 'line ' + job.cls;
out.appendChild(d);
var i = 0;
(function tick(){
d.textContent = text.slice(0, i);
i += 3;
window.scrollTo(0, document.body.scrollHeight);
if (i <= text.length + 2) setTimeout(tick, 11);
else { d.textContent = text; typing = false; pump(); }
})();
}
function echo(text, cls){ var d = document.createElement('div'); d.className = 'line ' + (cls || ''); d.textContent = text; out.appendChild(d); window.scrollTo(0, document.body.scrollHeight); }
function collected(){ var w = ''; for (var i = 0; i < CFG.puzzles.length; i++){ var L = (CFG.puzzles[i].letter || '').trim(); if (L) w += solved[i] ? L.toUpperCase() + ' ' : '_ '; } return w.trim() || '(niciuna)'; }
var bar = '==============================================';
var introLines = CFG._campaign
? [bar, ' ' + CFG.title.toUpperCase(), bar, 'Comenzi: INDICIU, AJUTOR. Scrie raspunsul si apasa Enter.']
: [bar, ' ' + CFG.title.toUpperCase(), bar, ' ', (CFG.player ? CFG.player + ', ' : '') + CFG.story, ' ', 'Comenzi: INDICIU, LITERE, AJUTOR. In rest, scrie raspunsul si apasa Enter.'];
say(introLines, '', nextPuzzle);
function nextPuzzle(){
idx++; attempts = 0; hintUsed = false;
if (idx >= CFG.puzzles.length) return finale();
var p = CFG.puzzles[idx];
var lines = [' ', '----------------------------------------------', '[' + (idx + 1) + '/' + CFG.puzzles.length + '] ' + (p.title || 'OBSTACOL').toUpperCase(), p.question];
if (p.type === 'tf') lines.push('(raspunde: ADEVARAT sau FALS)');
if (p.type === 'choice') { var o = choiceOpts(p); for (var i = 0; i < o.length; i++) lines.push(' ' + (i + 1) + ') ' + o[i]); }
say(lines);
}
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){}
});
return;
}
var w = finalWord().split('').join(' ');
var lines = [' ', bar, ' E V A D A R E R E U S I T A', bar, 'Stele: ' + totalStars + ' / ' + (CFG.puzzles.length * 3)];
if (w) lines.push('Cuvantul magic: ' + w);
lines.push((CFG.player ? CFG.player + ', ' : '') + CFG.finalMessage);
lines.push(' ');
lines.push('Scrie RESTART pentru a juca din nou.');
say(lines, 'ok');
beep(true);
}
cmd.addEventListener('keydown', function(e){
if (e.key !== 'Enter') return;
var v = cmd.value.trim();
cmd.value = '';
if (!v) return;
echo('> ' + v, 'dim');
var n = norm(v);
if (done) { if (n === 'restart') location.reload(); else echo('Scrie RESTART pentru a juca din nou.', 'dim'); return; }
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; }
if (n === 'litere') { say(['Litere adunate: ' + collected()]); return; }
var p = CFG.puzzles[idx];
if (!p) return;
if (n === 'indiciu' || n === 'hint') {
if (p.hint) { hintUsed = true; say(['INDICIU: ' + p.hint], 'warn'); }
else say(['Nu exista niciun indiciu aici.'], 'warn');
return;
}
var given = v;
if (p.type === 'choice') { var num = parseInt(v, 10); var o = choiceOpts(p); if (num >= 1 && o[num - 1]) given = o[num - 1]; }
if (p.type === 'tf') { if (n === 'a' || n === 'adevarat') given = 'Adevarat'; if (n === 'f' || n === 'fals') given = 'Fals'; }
if (checkAnswer(p, given)) {
var s = starsFor(attempts, hintUsed);
totalStars += s; solved[idx] = true; beep(true);
var ls = ['>> ACCES PERMIS. +' + s + ' stele (total ' + totalStars + ')'];
var L = (p.letter || '').trim();
if (L) ls.push('>> AI GASIT LITERA: ' + L.toUpperCase() + ' [' + collected() + ']');
say(ls, 'ok', nextPuzzle);
} else {
attempts++; beep(false);
say(['>> ACCES RESPINS. Mai incearca.'], 'bad');
}
});
roomReady();
<\/script>
</body>
</html>`;
}
/* ---------- motor: arcade pixel ---------- */
function gameArcade(cfg) {
return `<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(cfg.title)}</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: #0d0820; color: #fff; font-family: ui-monospace, "Courier New", monospace; display: flex; flex-direction: column; align-items: center; }
h1 { font-size: 17px; margin: 14px 0 6px; letter-spacing: .06em; text-transform: uppercase; }
#hud { display: flex; gap: 16px; align-items: center; font-size: 13px; color: #b9aee0; margin-bottom: 8px; flex-wrap: wrap; justify-content: center; padding: 0 10px; }
#hudLetters { display: flex; gap: 4px; }
#hudLetters span { width: 22px; height: 26px; border-radius: 5px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); display: flex; align-items: center; justify-content: center; font-weight: 700; color: rgba(255,255,255,.4); font-size: 13px; }
#hudLetters span.won { background: var(--accent); color: #fff; border-color: transparent; }
canvas { border: 3px solid #36246b; border-radius: 8px; background: #18102e; max-width: calc(100vw - 16px); image-rendering: pixelated; }
.help { font-size: 12px; color: #6f639e; margin: 8px 0 4px; text-align: center; padding: 0 10px; }
#dpad { display: flex; gap: 8px; margin: 6px 0 16px; flex-wrap: wrap; justify-content: center; }
#dpad button { width: 52px; height: 44px; font-size: 18px; border-radius: 9px; border: 1px solid #4a3590; background: #221643; color: #cdc3f0; cursor: pointer; }
#dpad button:active { background: var(--accent); }
#btnBomb { background: #7f1d1d; border-color: #b91c1c; }
#goOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.82); z-index: 25; align-items: center; justify-content: center; padding: 16px; }
#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; }
#goCard #goMsg { font-size: 20px; margin-bottom: 14px; }
#goCard button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 11px 18px; font-weight: 700; background: var(--accent); color: #fff; }
${SNIP.baseCss}${SNIP.modalCss}${SNIP.finalCss}
</style>
</head>
<body>
<h1>${esc(cfg.title)}</h1>
<div id="hud"><span id="hudStep"></span><span id="hudStars"></span><div id="hudLetters"></div></div>
<canvas id="cv"></canvas>
<div class="help">Sageti / WASD = misca, Space sau &#128163; = bomba. Sparge cutiile, evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.</div>
<div id="dpad"><button data-d="L">&#9664;</button><button data-d="U">&#9650;</button><button data-d="D">&#9660;</button><button data-d="R">&#9654;</button><button id="btnBomb">&#128163;</button></div>
<div id="goOverlay"><div id="goCard"><div id="goMsg"></div><button id="goRestart">Incearca din nou</button></div></div>
${SNIP.modalHtml}
${SNIP.finalHtml}
<script>
${libJS(cfg)}
var N = CFG.puzzles.length;
/* ===== Bomberman (S3 — port din scratch/bomberman-proto.html) =====
Păstrează contractul motorului: openPuzzle/onDoorSolved/showFinal/modalOpen/roomReady. */
var __seed = (typeof window.__seed === 'number') ? window.__seed : (Date.now() % 0xFFFFFF);
window.__seed = __seed;
function 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; }; }
var rng = makePRNG(__seed);
var GW = 15, GH = 13, TS = 36;
var T_FLOOR = 0, T_WALL = 1, T_BOX = 2, T_DOOR = 3, T_CHEST = 4;
var BOMB_TIMER = 2400, EXPLOSION_TIME = 500, EXPLOSION_RANGE = 3, ENEMY_INTERVAL = 600, RESPAWN_DELAY = 1500, INVINCIBLE_TIME = 2000, MAX_LIVES = 3;
var NUM_DOORS = N, NUM_ENEMIES = Math.max(2, Math.min(4, N + 1));
var map, player, enemies, bombs, explosions, lives, gameOver, gameWon, puzzleProgress, doorMeta, chestPos;
var animFrame, lastTime = 0, enemyTimer = 0, invincibleTimer = 0, bombIdCounter = 0;
var cv = el('cv'); cv.width = GW * TS; cv.height = GH * TS;
var ctx = cv.getContext('2d');
function 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; }
function buildMap(){
map = [];
for (var y = 0; y < GH; y++){ map[y] = []; for (var x = 0; x < 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; } }
var freeCells = [];
for (var fy = 1; fy < GH - 1; fy++) for (var fx = 1; fx < GW - 1; fx++) if (map[fy][fx] === T_FLOOR) freeCells.push({ x: fx, y: fy });
var safeZone = [{x:1,y:1},{x:2,y:1},{x:1,y:2}];
function isSafe(c){ for (var i = 0; i < safeZone.length; i++) if (safeZone[i].x === c.x && safeZone[i].y === c.y) return true; return false; }
var boxCandidates = freeCells.filter(function(c){ return !isSafe(c); });
shuffle(boxCandidates);
var boxCount = Math.floor(boxCandidates.length * 0.55);
for (var b = 0; b < boxCount; b++) map[boxCandidates[b].y][boxCandidates[b].x] = T_BOX;
var stillFree = [];
for (var sy = 1; sy < GH - 1; sy++) for (var sx = 1; sx < GW - 1; sx++) if (map[sy][sx] === T_FLOOR && !isSafe({x:sx,y:sy})) stillFree.push({ x: sx, y: sy });
shuffle(stillFree);
doorMeta = [];
for (var d = 0; d < NUM_DOORS && d < stillFree.length; d++){ var c = stillFree[d]; map[c.y][c.x] = T_DOOR; doorMeta.push({ x: c.x, y: c.y, id: d }); }
var chestCandidates = [];
for (var qy = 1; qy < GH - 1; qy++) for (var qx = 1; qx < 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) });
chestCandidates.sort(function(a,b){ return a.dist - b.dist; });
chestPos = chestCandidates.length > 0 ? chestCandidates[0] : { x: GW - 2, y: GH - 2 };
map[chestPos.y][chestPos.x] = T_CHEST;
}
function init(){
rng = makePRNG(__seed);
buildMap();
player = { x: 1, y: 1, alive: true, invincible: false };
var ec = [];
for (var y = 1; y < GH - 1; y++) for (var x = 1; x < GW - 1; x++) if (map[y][x] === T_FLOOR && (x > 3 || y > 3)) ec.push({ x: x, y: y });
shuffle(ec);
enemies = [];
for (var i = 0; i < NUM_ENEMIES && i < ec.length; i++) enemies.push({ x: ec[i].x, y: ec[i].y, alive: true, id: i });
bombs = []; explosions = []; lives = MAX_LIVES; gameOver = false; gameWon = false; enemyTimer = 0; invincibleTimer = 0; lastTime = 0;
if (!puzzleProgress) puzzleProgress = { doorsSolved: [] };
for (var dd = 0; dd < doorMeta.length; dd++) if (puzzleProgress.doorsSolved[dd]) map[doorMeta[dd].y][doorMeta[dd].x] = T_FLOOR;
hideGameOver();
updateHud();
if (animFrame) cancelAnimationFrame(animFrame);
animFrame = requestAnimationFrame(gameLoop);
}
function respawn(){
if (lives <= 0){ showGameOver(); return; }
player = { x: 1, y: 1, alive: true, invincible: true };
bombs = []; explosions = []; invincibleTimer = INVINCIBLE_TIME; gameOver = false;
updateHud();
}
function showGameOver(){ gameOver = true; el('goMsg').textContent = '\\ud83d\\udc80 Ai ramas fara vieti!'; el('goOverlay').style.display = 'flex'; }
function hideGameOver(){ el('goOverlay').style.display = 'none'; }
el('goRestart').onclick = function(){ init(); };
/* ----- HUD (motor: hudStep/hudStars/hudLetters) ----- */
function updateHud(){
var solved = puzzleProgress ? puzzleProgress.doorsSolved.filter(Boolean).length : 0;
var alive = enemies ? enemies.filter(function(e){ return e.alive; }).length : 0;
el('hudStep').textContent = '\\u2764\\ufe0f ' + lives + ' \\ud83d\\udc7e ' + alive + ' \\ud83d\\udd13 ' + solved + '/' + N;
el('hudStars').textContent = totalStars + ' \\u2605';
var hb = el('hudLetters'); hb.innerHTML = '';
for (var j = 0; j < 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); }
}
/* ----- Bombe + explozii în lanț ----- */
function placeBomb(){
if (!player.alive || gameOver || gameWon || modalOpen()) return;
if (bombs.length >= 1) return;
bombs.push({ x: player.x, y: player.y, timer: BOMB_TIMER, id: bombIdCounter++ });
updateHud();
}
function explodeBomb(bomb){
bombs = bombs.filter(function(b){ return b.id !== bomb.id; });
var cells = [{ x: bomb.x, y: bomb.y }];
var dirs = [[1,0],[-1,0],[0,1],[0,-1]];
for (var d = 0; d < dirs.length; d++){ var dx = dirs[d][0], dy = dirs[d][1]; for (var r = 1; r <= EXPLOSION_RANGE; r++){ var cx = bomb.x + dx * r, cy = bomb.y + dy * r; if (cx < 0 || cy < 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; } }
explosions.push({ cells: cells, endTime: performance.now() + EXPLOSION_TIME });
var chain = bombs.slice();
for (var i = 0; i < chain.length; i++){ var bb = chain[i]; for (var c = 0; c < cells.length; c++) if (cells[c].x === bb.x && cells[c].y === bb.y){ explodeBomb(bb); break; } }
checkExplosionHits(cells);
updateHud();
}
function checkExplosionHits(cells){
for (var c = 0; c < cells.length; c++){ var cx = cells[c].x, cy = cells[c].y; for (var i = 0; i < 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(); }
}
function killPlayer(){
if (!player.alive) return;
player.alive = false; lives--; updateHud();
setTimeout(function(){ if (lives > 0) respawn(); else showGameOver(); }, RESPAWN_DELAY);
}
/* ----- Mișcare jucător + uși (puzzle) / cufăr (scăpare) ----- */
function movePlayer(dir){
if (!player.alive || gameOver || gameWon || modalOpen()) return;
var dx = 0, dy = 0;
if (dir === 'U') dy = -1; else if (dir === 'D') dy = 1; else if (dir === 'L') dx = -1; else if (dir === 'R') dx = 1;
var nx = player.x + dx, ny = player.y + dy;
if (nx < 0 || ny < 0 || nx >= GW || ny >= GH) return;
var t = map[ny][nx];
if (t === T_WALL || t === T_BOX) return;
for (var i = 0; i < bombs.length; i++) if (bombs[i].x === nx && bombs[i].y === ny) return;
if (t === T_DOOR){ for (var d = 0; d < doorMeta.length; d++) if (doorMeta[d].x === nx && doorMeta[d].y === ny){ if (!puzzleProgress.doorsSolved[d]) openPuzzle(d, onDoorSolved); return; } return; }
if (t === T_CHEST){ player.x = nx; player.y = ny; gameWon = true; showFinal(); return; }
player.x = nx; player.y = ny;
checkPlayerEnemyCollision();
}
function onDoorSolved(id){
if (!puzzleProgress) puzzleProgress = { doorsSolved: [] };
puzzleProgress.doorsSolved[id] = true;
if (doorMeta && doorMeta[id]) map[doorMeta[id].y][doorMeta[id].x] = T_FLOOR;
updateHud();
}
/* ----- AI dușmani: BFS spre jucător (doar pe podea) ----- */
function moveEnemies(){ if (gameOver || gameWon) return; for (var i = 0; i < 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(); }
function bfsStep(sx, sy, tx, ty){
if (sx === tx && sy === ty) return null;
var visited = {}; var queue = [{ x: sx, y: sy, step: null }]; visited[sy + ',' + sx] = true;
var dirs = [[1,0],[-1,0],[0,1],[0,-1]];
while (queue.length > 0){ var cur = queue.shift(); for (var d = 0; d < dirs.length; d++){ var nx = cur.x + dirs[d][0], ny = cur.y + dirs[d][1]; var key = ny + ',' + nx; if (nx < 0 || ny < 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 < 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 < 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 }); } }
return null;
}
function checkPlayerEnemyCollision(){ if (!player.alive || player.invincible) return; for (var i = 0; i < enemies.length; i++) if (enemies[i].alive && enemies[i].x === player.x && enemies[i].y === player.y){ killPlayer(); return; } }
/* ----- Game loop ----- */
function gameLoop(now){
var dt = now - (lastTime || now); lastTime = now;
if (!gameOver && !gameWon){
if (player.invincible && invincibleTimer > 0){ invincibleTimer -= dt; if (invincibleTimer <= 0){ player.invincible = false; invincibleTimer = 0; checkPlayerEnemyCollision(); } }
var explodeList = [];
for (var i = 0; i < bombs.length; i++){ bombs[i].timer -= dt; if (bombs[i].timer <= 0) explodeList.push(bombs[i]); }
for (var k = 0; k < explodeList.length; k++) explodeBomb(explodeList[k]);
var nowMs = performance.now();
explosions = explosions.filter(function(ex){ return ex.endTime > nowMs; });
if (!modalOpen() && player.alive){ enemyTimer += dt; if (enemyTimer >= ENEMY_INTERVAL){ enemyTimer = 0; moveEnemies(); } }
}
draw(now); updateHud();
animFrame = requestAnimationFrame(gameLoop);
}
/* ----- Desenare ----- */
function draw(now){
ctx.clearRect(0, 0, cv.width, cv.height);
var expSet = {}; var nowMs = performance.now();
for (var ex = 0; ex < explosions.length; ex++) if (explosions[ex].endTime > nowMs){ var cs = explosions[ex].cells; for (var c = 0; c < cs.length; c++) expSet[cs[c].y + ',' + cs[c].x] = true; }
for (var y = 0; y < GH; y++) for (var x = 0; x < GW; x++){ var px = x * TS, py = y * TS, t = map[y][x], isExp = expSet[y + ',' + x];
if (t === T_WALL) drawWall(px, py, y);
else if (t === T_BOX) drawBox(px, py, isExp);
else if (t === T_DOOR){ drawFloor(px, py, x, y, isExp); drawDoor(px, py); }
else if (t === T_CHEST){ drawFloor(px, py, x, y, isExp); drawChest(px, py); }
else drawFloor(px, py, x, y, isExp);
}
for (var bi = 0; bi < bombs.length; bi++) drawBomb(bombs[bi], now);
for (var en = 0; en < enemies.length; en++) if (enemies[en].alive) drawEnemy(enemies[en]);
if (player.alive) drawPlayer(now);
}
function 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); }
function 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); } }
function 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(); }
function 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); }
function 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); }
function 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(); }
function 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); }
function 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); }
/* ----- Input ----- */
window.addEventListener('keydown', function(e){
if (modalOpen()) return;
var dir = { ArrowUp:'U', ArrowDown:'D', ArrowLeft:'L', ArrowRight:'R', w:'U', s:'D', a:'L', d:'R' }[e.key];
if (dir){ e.preventDefault(); movePlayer(dir); return; }
if (e.key === ' ' || e.key === 'b' || e.key === 'B'){ e.preventDefault(); placeBomb(); }
});
document.querySelectorAll('#dpad button[data-d]').forEach(function(b){ b.addEventListener('click', function(){ movePlayer(b.dataset.d); }); });
el('btnBomb').addEventListener('click', function(){ placeBomb(); });
/* ----- Hooks de test (window.__game) ----- */
window.__game = {
get lives(){ return lives; },
get enemiesCount(){ return enemies ? enemies.filter(function(e){ return e.alive; }).length : 0; },
get puzzleProgress(){ return puzzleProgress; },
get bombs(){ return bombs ? bombs.slice() : []; },
get gameOver(){ return gameOver; },
get gameWon(){ return gameWon; },
get player(){ return player ? { x: player.x, y: player.y, alive: player.alive, invincible: player.invincible } : null; },
get map(){ return map ? map.map(function(r){ return r.slice(); }) : []; },
get enemies(){ return enemies ? enemies.slice() : []; },
get explosions(){ return explosions ? explosions.slice() : []; },
placeBomb: function(){ placeBomb(); },
movePlayer: function(dir){ movePlayer(dir); },
explodeAllBombs: function(){ var list = bombs.slice(); for (var i = 0; i < list.length; i++) explodeBomb(list[i]); },
spawnEnemyAt: function(x, y){ enemies.push({ x: x, y: y, alive: true, id: 999 + enemies.length }); },
killPlayer: function(){ killPlayer(); },
restartWithSeed: function(seed){ __seed = seed; window.__seed = seed; puzzleProgress = null; init(); },
getDoorAt: function(x, y){ for (var d = 0; d < doorMeta.length; d++) if (doorMeta[d].x === x && doorMeta[d].y === y) return d; return -1; },
solveDoor: function(id){ onDoorSolved(id); },
teleportPlayer: function(x, y){ player.x = x; player.y = y; },
bfsStep: function(sx, sy, tx, ty){ return bfsStep(sx, sy, tx, ty); },
setTile: function(x, y, t){ if (map && map[y]) map[y][x] = t; },
getTile: function(x, y){ return map && map[y] ? map[y][x] : -1; }
};
${SNIP.modalJs}
${SNIP.finalJs}
init();
roomReady();
<\/script>
</body>
</html>`;
}
/* ---------- motor: story chat ---------- */
function gameChat(cfg) {
return `<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(cfg.title)}</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; background: #0b1220; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #e5e7eb; display: flex; justify-content: center; min-height: 100vh; }
#app { width: 100%; max-width: 480px; height: 100vh; height: 100dvh; display: flex; flex-direction: column; background: #0f172a; }
header { display: flex; gap: 10px; align-items: center; padding: 10px 14px; background: #1e293b; border-bottom: 1px solid #334155; }
.avatar { width: 38px; height: 38px; border-radius: 50%; background: var(--accent); display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 18px; color: #fff; }
.cname { font-weight: 700; }
.cstatus { font-size: 12px; color: #34d399; }
#msgs { flex: 1; overflow-y: auto; padding: 14px 12px; display: flex; flex-direction: column; gap: 8px; }
.row { display: flex; }
.row.me { justify-content: flex-end; }
.bub { max-width: 78%; padding: 9px 13px; border-radius: 16px; line-height: 1.4; font-size: 15px; white-space: pre-line; animation: bin .25s ease; }
@keyframes bin { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } }
.row.him .bub { background: #1e293b; border-bottom-left-radius: 5px; }
.row.me .bub { background: var(--accent); color: #fff; border-bottom-right-radius: 5px; }
.bub.tile { font-size: 24px; font-weight: 800; letter-spacing: 2px; background: #14532d; border: 1px solid #22c55e; }
.bub.typing i { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #94a3b8; margin: 0 2px; animation: tp 1s infinite; }
.bub.typing i:nth-child(2) { animation-delay: .15s; } .bub.typing i:nth-child(3) { animation-delay: .3s; }
@keyframes tp { 30% { transform: translateY(-5px); } }
#composer { padding: 10px 12px; background: #1e293b; border-top: 1px solid #334155; display: flex; flex-wrap: wrap; gap: 8px; min-height: 58px; }
#composer input { flex: 1; min-width: 120px; font: inherit; font-size: 15px; padding: 9px 13px; border-radius: 99px; border: 1px solid #475569; background: #0f172a; color: #fff; }
#composer input:focus { outline: none; border-color: var(--accent); }
#composer button { font: inherit; cursor: pointer; border: none; border-radius: 99px; padding: 9px 16px; font-weight: 600; background: var(--accent); color: #fff; }
#composer button.chip { background: #0f172a; border: 1px solid #475569; color: #cbd5e1; }
#composer button.chip:hover { border-color: var(--accent); color: #fff; }
${SNIP.baseCss}${SNIP.finalCss}
</style>
</head>
<body>
<div id="app">
<header><div class="avatar" id="av"></div><div><div class="cname" id="cn"></div><div class="cstatus" id="cs">online</div></div></header>
<div id="msgs"></div>
<div id="composer"></div>
</div>
${SNIP.finalHtml}
<script>
${libJS(cfg)}
var who = (CFG.charName || 'Alex').trim() || 'Alex';
el('cn').textContent = who; el('av').textContent = who.charAt(0).toUpperCase();
var msgs = el('msgs'), composer = el('composer');
var idx = -1, attempts = 0, hintUsed = false;
var wrongs = ['Nu... nu a mers. Mai incearca!', 'Hmm, nu e asta. Gandeste-te bine!', 'Tot incuiat. Alta idee?'];
function scrollEnd(){ msgs.scrollTop = msgs.scrollHeight; }
function bubble(side, text, cls){
var r = document.createElement('div'); r.className = 'row ' + side;
var b = document.createElement('div'); b.className = 'bub' + (cls ? ' ' + cls : '');
b.textContent = text;
r.appendChild(b); msgs.appendChild(r); scrollEnd();
return b;
}
function charMsg(text, cb){
el('cs').textContent = 'scrie...';
var b = bubble('him', '', 'typing');
b.innerHTML = '<i></i><i></i><i></i>';
var d = Math.min(450 + text.length * 14, 1800);
setTimeout(function(){
b.className = 'bub'; b.textContent = text;
el('cs').textContent = 'online'; scrollEnd();
if (cb) setTimeout(cb, 280);
}, d);
}
function seq(texts, cb){ var i = 0; (function n(){ if (i >= texts.length) { if (cb) cb(); return; } charMsg(texts[i++], n); })(); }
function storyChunks(){
var parts = (CFG.story || '').match(/[^.!?]+[.!?]*\\s*/g) || [];
var out = [], cur = '';
for (var i = 0; i < parts.length; i++) {
if (cur && (cur + parts[i]).length > 110) { out.push(cur.trim()); cur = ''; }
cur += parts[i];
}
if (cur.trim()) out.push(cur.trim());
return out.length ? out : [CFG.story || ''];
}
function setComposer(p){
composer.innerHTML = '';
function chip(label, fn, cls){ var b = document.createElement('button'); if (cls) b.className = cls; b.textContent = label; b.onclick = fn; composer.appendChild(b); return b; }
if (p.type === 'free') {
var inp = document.createElement('input'); inp.placeholder = 'Scrie raspunsul...'; inp.autocomplete = 'off';
composer.appendChild(inp);
var send = chip('Trimite', function(){ if (inp.value.trim()) { var v = inp.value.trim(); inp.value = ''; answer(v); } });
inp.onkeydown = function(e){ if (e.key === 'Enter') send.click(); };
setTimeout(function(){ inp.focus(); }, 100);
} else if (p.type === 'tf') {
chip('Adevarat', function(){ answer('Adevarat'); }, 'chip');
chip('Fals', function(){ answer('Fals'); }, 'chip');
} else {
choiceOpts(p).forEach(function(o){ chip(o, function(){ answer(o); }, 'chip'); });
}
if (p.hint) chip('Cere un indiciu', function(){ hintUsed = true; bubble('me', 'Ai vreun indiciu?'); composer.innerHTML = ''; charMsg(p.hint, function(){ setComposer(p); }); }, 'chip');
}
function answer(given){
var p = CFG.puzzles[idx];
bubble('me', given);
composer.innerHTML = '';
if (checkAnswer(p, given)) {
var s = starsFor(attempts, hintUsed);
totalStars += s; beep(true);
var L = (p.letter || '').trim();
charMsg('Da! Asta era! (+' + s + ' \\u2605, total ' + totalStars + ')', function(){
if (L) { bubble('him', L.toUpperCase(), 'tile'); charMsg('Am gasit o litera!', next); }
else next();
});
} else {
attempts++; beep(false);
charMsg(wrongs[(attempts - 1) % wrongs.length], function(){ setComposer(p); });
}
}
function next(){
idx++; attempts = 0; hintUsed = false;
if (idx >= CFG.puzzles.length) {
seq(['AM IESIT! Multumesc' + (CFG.player ? ', ' + CFG.player : '') + '!', CFG.finalMessage || ''], function(){ showFinal(); });
return;
}
var p = CFG.puzzles[idx];
seq([(p.title ? p.title + '. ' : '') + p.question], function(){ setComposer(p); });
}
var chatIntro = CFG._campaign
? ['Camera ' + (CFG._campaign.idx + 1) + ' din ' + CFG._campaign.total + '. Sa incepem!']
: ['Salut' + (CFG.player ? ', ' + CFG.player : '') + '!'].concat(storyChunks()).concat(['Ma ajuti sa ies de aici?']);
seq(chatIntro, next);
${SNIP.finalJs}
roomReady();
<\/script>
</body>
</html>`;
}
/* ---------- motor: point-and-click ---------- */
function gamePoint(cfg) {
return `<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(cfg.title)}</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: #0d0820; color: #fff; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; display: flex; flex-direction: column; align-items: center; }
h1 { font-size: 19px; margin: 14px 0 4px; }
#hud { display: flex; gap: 16px; align-items: center; font-size: 13px; color: #b9aee0; margin-bottom: 4px; flex-wrap: wrap; justify-content: center; padding: 0 10px; }
#hudLetters { display: flex; gap: 4px; }
#hudLetters span { width: 22px; height: 26px; border-radius: 5px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); display: flex; align-items: center; justify-content: center; font-weight: 700; color: rgba(255,255,255,.4); font-size: 13px; }
#hudLetters span.won { background: var(--accent); color: #fff; border-color: transparent; }
.note { font-size: 13px; color: #8d80bb; margin: 2px 0 10px; text-align: center; padding: 0 12px; min-height: 18px; }
#stage { width: 100%; max-width: 860px; padding: 0 10px 20px; }
svg { width: 100%; height: auto; border-radius: 12px; box-shadow: 0 14px 40px rgba(0,0,0,.5); display: block; }
.hot { cursor: pointer; }
.hot:hover { filter: brightness(1.35) drop-shadow(0 0 6px rgba(255,255,255,.35)); }
.hot.done { opacity: .6; cursor: default; }
.hot.done:hover { filter: none; }
#door { cursor: pointer; }
#door.open { filter: drop-shadow(0 0 12px #22c55e); }
${SNIP.baseCss}${SNIP.modalCss}${SNIP.finalCss}
</style>
</head>
<body>
<h1>${esc(cfg.title)}</h1>
<div id="hud"><span id="hudStep"></span><span id="hudStars"></span><div id="hudLetters"></div></div>
<div class="note" id="note">Cerceteaza camera: da click pe obiecte si rezolva-le ca sa deschizi usa.</div>
<div id="stage"><svg id="scene" viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg"></svg></div>
${SNIP.modalHtml}
${SNIP.finalHtml}
<script>
${libJS(cfg)}
var N = CFG.puzzles.length, solvedFlags = [], solvedCount = 0;
var POOL = [
{ name: 'Ceasul', x: 308, y: 62, svg: '<circle cx="32" cy="32" r="30" fill="#caa856"/><circle cx="32" cy="32" r="24" fill="#f4ecd8"/><line x1="32" y1="32" x2="32" y2="16" stroke="#222" stroke-width="3"/><line x1="32" y1="32" x2="43" y2="36" stroke="#222" stroke-width="3"/><circle cx="32" cy="32" r="2.5" fill="#222"/>' },
{ name: 'Tabloul', x: 108, y: 84, svg: '<rect width="84" height="64" fill="#8a5a2b"/><rect x="6" y="6" width="72" height="52" fill="#b7d3e8"/><polygon points="10,52 34,24 50,44 60,32 74,52" fill="#4f7a4f"/><circle cx="62" cy="16" r="6" fill="#f7d774"/>' },
{ name: 'Sertarul biroului', x: 64, y: 300, svg: '<rect width="150" height="14" fill="#7a4a22"/><rect x="6" y="14" width="138" height="46" fill="#925a2c"/><rect x="20" y="22" width="110" height="28" fill="#7a4a22"/><circle cx="75" cy="36" r="4" fill="#f3cf6d"/><rect x="10" y="60" width="10" height="34" fill="#7a4a22"/><rect x="130" y="60" width="10" height="34" fill="#7a4a22"/>' },
{ name: 'Dulapul', x: 232, y: 228, svg: '<rect width="92" height="142" fill="#8a5a2b"/><rect x="5" y="5" width="38" height="132" fill="#a06a35"/><rect x="49" y="5" width="38" height="132" fill="#a06a35"/><circle cx="38" cy="72" r="3.5" fill="#f3cf6d"/><circle cx="54" cy="72" r="3.5" fill="#f3cf6d"/>' },
{ name: 'Fereastra', x: 424, y: 62, svg: '<rect width="96" height="120" fill="#6b7f9e"/><rect x="6" y="6" width="84" height="108" fill="#101d3a"/><line x1="48" y1="6" x2="48" y2="114" stroke="#6b7f9e" stroke-width="5"/><line x1="6" y1="60" x2="90" y2="60" stroke="#6b7f9e" stroke-width="5"/><circle cx="68" cy="32" r="11" fill="#f4f1de"/>' },
{ name: 'Raftul cu carti', x: 558, y: 78, svg: '<rect y="34" width="120" height="8" fill="#7a4a22"/><rect x="6" y="6" width="13" height="28" fill="#b54a4a"/><rect x="21" y="2" width="13" height="32" fill="#4a7ab5"/><rect x="36" y="8" width="13" height="26" fill="#54a05e"/><rect x="51" y="4" width="13" height="30" fill="#c2a23e"/><rect x="66" y="9" width="13" height="25" fill="#9a5ab5"/><rect x="81" y="5" width="13" height="29" fill="#b5764a"/><rect x="96" y="8" width="13" height="26" fill="#5aa0b5"/>' },
{ name: 'Cutia', x: 404, y: 330, svg: '<rect width="66" height="50" fill="#925a2c"/><line x1="0" y1="0" x2="66" y2="50" stroke="#7a4a22" stroke-width="5"/><line x1="66" y1="0" x2="0" y2="50" stroke="#7a4a22" stroke-width="5"/><rect width="66" height="50" fill="none" stroke="#7a4a22" stroke-width="6"/>' },
{ name: 'Lampa', x: 516, y: 252, svg: '<polygon points="14,0 50,0 60,34 4,34" fill="#d9a23e"/><rect x="29" y="34" width="6" height="78" fill="#555"/><rect x="12" y="112" width="40" height="8" rx="3" fill="#555"/><circle cx="32" cy="17" r="9" fill="#ffe9a8" opacity=".8"/>' },
{ name: 'Seiful', x: 596, y: 300, svg: '<rect width="74" height="74" rx="6" fill="#5b6470"/><rect x="7" y="7" width="60" height="60" rx="4" fill="#434b55"/><circle cx="37" cy="37" r="14" fill="#5b6470"/><circle cx="37" cy="37" r="9" fill="#2c3138"/><line x1="37" y1="28" x2="37" y2="37" stroke="#d4d4d4" stroke-width="2.5"/>' },
{ name: 'Covorul', x: 250, y: 432, svg: '<ellipse cx="110" cy="26" rx="110" ry="26" fill="#7a3b56"/><ellipse cx="110" cy="26" rx="80" ry="17" fill="#94506c"/><ellipse cx="110" cy="26" rx="46" ry="9" fill="#7a3b56"/>' }
];
function crate(i){ return { name: 'Lada ' + (i + 1), x: 50 + ((i - 10) % 8) * 86, y: 408, svg: POOL[6].svg }; }
var base = '<rect width="800" height="380" fill="#3b2a63"/><rect y="380" width="800" height="120" fill="#241a3f"/><rect y="372" width="800" height="8" fill="#1c1336"/>'
+ '<g id="door"><rect x="694" y="148" width="86" height="232" fill="#6b4226"/><rect x="702" y="156" width="70" height="216" fill="#8a5a2b"/><circle cx="712" cy="266" r="5" fill="#f3cf6d"/><g id="lock"><rect x="730" y="250" width="26" height="22" rx="4" fill="#caa856"/><path d="M735 250 v-7 a8 8 0 0 1 16 0 v7" fill="none" stroke="#caa856" stroke-width="4"/></g></g>';
var objs = '';
for (var i = 0; i < N; i++) {
var o = POOL[i] || crate(i);
objs += '<g class="hot" data-i="' + i + '" transform="translate(' + o.x + ',' + o.y + ')">' + o.svg + '<title>' + o.name + '</title></g>';
}
el('scene').innerHTML = base + objs;
el('scene').addEventListener('click', function(e){
var t = e.target;
var g = t.closest ? t.closest('g.hot') : null;
if (g) {
if (!g.classList.contains('done')) openPuzzle(+g.getAttribute('data-i'), onSolved);
return;
}
var door = t.closest ? t.closest('#door') : null;
if (door) {
if (solvedCount >= N) { showFinal(); }
else { beep(false); el('note').textContent = 'Usa e incuiata! Mai ai ' + (N - solvedCount) + ' obiecte de cercetat.'; }
}
});
function onSolved(i){
solvedFlags[i] = true; solvedCount++;
var g = document.querySelector('g.hot[data-i="' + i + '"]');
g.classList.add('done');
var L = (CFG.puzzles[i].letter || '').trim();
g.innerHTML += '<circle cx="0" cy="0" r="13" fill="#16a34a"/><text x="0" y="5" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">' + (L ? L.toUpperCase() : '\\u2713') + '</text>';
updateHud();
if (solvedCount >= N) {
el('door').classList.add('open');
el('note').textContent = 'Toate obiectele rezolvate! Da click pe usa ca sa evadezi.';
beep(true);
}
}
function updateHud(){
el('hudStep').textContent = 'Obiecte: ' + solvedCount + '/' + N;
el('hudStars').textContent = totalStars + ' \\u2605';
var hb = el('hudLetters'); hb.innerHTML = '';
for (var j = 0; j < N; j++) {
var L = (CFG.puzzles[j].letter || '').trim();
if (!L) continue;
var s = document.createElement('span');
if (solvedFlags[j]) { s.textContent = L.toUpperCase(); s.className = 'won'; }
else s.textContent = '?';
hb.appendChild(s);
}
}
${SNIP.modalJs}
${SNIP.finalJs}
updateHud();
roomReady();
<\/script>
</body>
</html>`;
}
/* ---------- motor: campanie multi-stil ----------
*
* ASCII DIAGRAM — contractul parent.*:
* ┌──────────── orchestrator (window) ─────────────────────────┐
* │ nextRoom({idx,stars,letter}) ← camera.parent.nextRoom │
* │ roomReady(idx) ← camera.parent.roomReady │
* │ roomError(idx,msg) ← camera.parent.roomError │
* │ beep(ok) ← camera.parent.beep │
* └────────────────────────────────────────────────────────────┘
* idx activ deținut de orchestrator; frame stale ignorate (D5).
* nextRoom acceptat o singură dată per idx (idempotență, D4).
* Timeout 4s fără roomReady → skip cameră (T3).
* srcdoc = TPL[stil].replace(TOKEN, fn) — funcție nu string (D1).
* json = JSON.stringify(cfg).replace(/</g,'\\u003c') — D6.
*/
function gameCampaign(cfg) {
const ROTATION = ['classic', 'terminal', 'arcade', 'chat', 'point'];
/* Template-uri per stil: fiecare motor generat O dată cu sentinel __CFG__ */
const stylesNeeded = new Set(cfg.puzzles.map((p, i) => p.style || ROTATION[i % ROTATION.length]));
const engines = { classic: gameClassic, terminal: gameTerminal, arcade: gameArcade, chat: gameChat, point: gamePoint };
const templates = {};
for (const style of stylesNeeded) {
templates[style] = (engines[style] || gameClassic)('__TEMPLATE__');
}
const tplJson = JSON.stringify(templates).replace(/</g, '\\u003c');
const masterJson = JSON.stringify(cfg).replace(/</g, '\\u003c');
const nRooms = cfg.puzzles.length;
const nStyles = stylesNeeded.size;
return `<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(cfg.title)}</title>
<style>
/*
* ASCII DIAGRAM — contractul parent.*:
* ┌──────────── orchestrator (window) ─────────────────────────┐
* │ nextRoom({idx,stars,letter}) ← camera.parent.nextRoom │
* │ roomReady(idx) ← camera.parent.roomReady │
* │ roomError(idx,msg) ← camera.parent.roomError │
* │ beep(ok) ← camera.parent.beep │
* └────────────────────────────────────────────────────────────┘
*/
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; overflow: hidden; }
body {
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
background: var(--c-bg, #0d0620); color: var(--c-ink, #fff);
display: flex; flex-direction: column;
--c-bg: #0d0620; --c-surface: #221440; --c-line: rgba(255,255,255,.18);
--c-ink: #fff; --c-gold: #fbbf24;
}
#chrome {
height: 48px; min-height: 48px; flex-shrink: 0;
background: #1a0e3a; border-bottom: 1px solid rgba(255,255,255,.15);
display: flex; align-items: center; padding: 0 16px; gap: 12px;
}
#chrome-title { font-size: 15px; font-weight: 700; }
#chrome .sp { flex: 1; }
#dots { display: flex; gap: 8px; }
#dots span {
width: 10px; height: 10px; border-radius: 50%;
background: rgba(255,255,255,.2); transition: background .3s; display: inline-block;
}
#dots span.active { background: #a78bfa; }
#dots span.done { background: var(--c-gold); }
#room-wrap { flex: 1; position: relative; min-height: 0; }
#room-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* Overlay-uri */
.overlay {
display: none; position: absolute; inset: 0;
flex-direction: column; align-items: center; justify-content: center;
gap: 18px; padding: 24px; text-align: center; overflow-y: auto;
}
.overlay.show { display: flex; }
/* Intro */
#intro { background: var(--c-bg); }
#intro h1 { margin: 0; font-size: clamp(22px,5vw,36px); font-weight: 900; }
#intro .story-text { color: rgba(255,255,255,.8); max-width: 56ch; line-height: 1.6; }
#intro .promise { color: rgba(255,255,255,.5); font-size: 14px; }
/* Coridor */
#corridor { background: var(--c-bg); }
#corr-reward { display: flex; align-items: center; gap: 16px; }
#corr-stars { font-size: 26px; letter-spacing: 3px; color: var(--c-gold); }
#corr-letter { font-size: 56px; font-weight: 900; color: var(--c-gold); line-height: 1; }
#corr-label { color: rgba(255,255,255,.6); font-size: 13px; }
#corr-next { color: rgba(255,255,255,.75); font-size: 15px; font-weight: 600; }
#corr-door { display: flex; align-items: center; justify-content: center; flex: 1; min-height: 0; padding: 8px 0; }
/* Skip */
#skip-banner { background: var(--c-bg); }
/* ===== UȘILE — 5 stiluri × 3 stări ===== */
:root { --c-gold: #fbbf24; }
/* Common */
.door-lock {
position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,.65); border-radius: 6px; padding: 2px 7px;
font-size: 13px; line-height: 1.4; z-index: 2; pointer-events: none;
}
@keyframes door-open {
0% { transform: scale(1) rotateY(0deg); opacity: 1; }
50% { transform: scale(1.06) rotateY(-30deg); opacity: .8; }
100% { transform: scale(.85) rotateY(-90deg); opacity: 0; }
}
.opening { animation: door-open .25s cubic-bezier(.4,0,1,1) forwards; transform-origin: left center; perspective: 600px; }
@media (prefers-reduced-motion: reduce) { .opening { animation: none; opacity: 0; } }
/* Classic */
.door-classic {
width: 88px; height: 124px; position: relative;
background: #fff; border-radius: 10px; border: 1.5px solid rgba(0,0,0,.06);
box-shadow: 0 8px 32px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.9);
display: flex; align-items: center; justify-content: center;
transition: transform .25s cubic-bezier(.22,1,.36,1), box-shadow .25s, filter .25s;
}
.door-classic .dq { font-size: 50px; font-weight: 900; line-height: 1; color: var(--c-gold); text-shadow: 0 2px 14px rgba(251,191,36,.6); user-select: none; }
.door-classic::after { content: ''; position: absolute; right: 12px; top: 50%; width: 8px; height: 8px; border-radius: 50%; background: rgba(0,0,0,.22); margin-top: -4px; }
.door-classic.stuck { filter: grayscale(1) brightness(.6); }
.door-classic.crescendo { transform: scale(1.35); box-shadow: 0 0 0 3px var(--c-gold), 0 14px 48px rgba(251,191,36,.35), 0 8px 32px rgba(0,0,0,.55); }
/* Terminal */
.door-terminal {
width: 88px; height: 124px; position: relative;
background: #000; border: 2px solid #39ff6e; overflow: hidden;
box-shadow: 0 0 16px rgba(57,255,110,.4), inset 0 0 12px rgba(57,255,110,.07);
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
transition: transform .25s cubic-bezier(.22,1,.36,1), box-shadow .25s, filter .25s;
}
.door-terminal::before { content: ''; position: absolute; inset: 0; pointer-events: none; background: repeating-linear-gradient(0deg, rgba(0,0,0,.32) 0 1px, transparent 1px 3px); }
.door-terminal .dt-txt { font: 700 10px/1.2 "Courier New", monospace; letter-spacing: .12em; color: #39ff6e; text-shadow: 0 0 8px rgba(57,255,110,.75); z-index: 1; }
.door-terminal .dt-cur { font: 16px/1 "Courier New", monospace; color: #39ff6e; text-shadow: 0 0 8px rgba(57,255,110,.75); animation: dt-blink 1s step-end infinite; z-index: 1; }
@keyframes dt-blink { 0%,100% { opacity: 1; } 50% { opacity: 0; } }
.door-terminal.stuck { filter: grayscale(1) brightness(.55); border-color: #444; box-shadow: none; }
.door-terminal.stuck .dt-cur { animation: none; opacity: 0; }
.door-terminal.crescendo { transform: scale(1.35); box-shadow: 0 0 0 2px #39ff6e, 0 0 32px rgba(57,255,110,.7), inset 0 0 18px rgba(57,255,110,.14); }
/* Arcade */
.door-arcade {
width: 88px; height: 124px; position: relative;
background: #18102e; border: 4px solid #4ade80;
box-shadow: inset 0 0 0 4px #166534, 0 0 0 4px #0a1606, 0 8px 24px rgba(0,0,0,.7);
display: flex; align-items: center; justify-content: center; image-rendering: pixelated;
transition: transform .25s cubic-bezier(.22,1,.36,1), box-shadow .25s, filter .25s;
}
.door-arcade::after { content: ''; position: absolute; right: 12px; top: 50%; margin-top: -5px; width: 10px; height: 10px; background: var(--c-gold); box-shadow: inset -2px -2px 0 #b45309, inset 1px 1px 0 #fde68a; }
.door-arcade .da-sprite { font: 900 24px/1 ui-monospace, monospace; color: #4ade80; text-shadow: 0 0 10px rgba(74,222,128,.55); user-select: none; }
.door-arcade.stuck { filter: grayscale(1) brightness(.55); box-shadow: none; }
.door-arcade.crescendo { transform: scale(1.35); box-shadow: inset 0 0 0 4px #166534, 0 0 0 4px #0a1606, 0 0 0 8px #4ade80, 0 0 28px rgba(74,222,128,.5), 0 8px 24px rgba(0,0,0,.7); }
/* Chat */
.door-chat {
width: 72px; height: 124px; position: relative;
background: linear-gradient(165deg, #1d4ed8 0%, #1e3a8a 100%);
border-radius: 12px; border: 1.5px solid rgba(255,255,255,.14);
box-shadow: 0 8px 28px rgba(29,78,216,.5), inset 0 1px 0 rgba(255,255,255,.2);
display: flex; flex-direction: column; align-items: center; padding: 8px 6px 10px; gap: 5px;
transition: transform .25s cubic-bezier(.22,1,.36,1), box-shadow .25s, filter .25s;
}
.door-chat .dc-notch { width: 18px; height: 4px; background: rgba(0,0,0,.4); border-radius: 2px; flex-shrink: 0; }
.door-chat .dc-screen { flex: 1; width: 100%; background: #0f172a; border-radius: 7px; padding: 7px 5px; display: flex; flex-direction: column; gap: 5px; overflow: hidden; }
.door-chat .dc-bub { border-radius: 8px; padding: 4px 7px; font-size: 9px; color: rgba(255,255,255,.85); line-height: 1.2; max-width: 80%; }
.door-chat .dc-npc { background: #1e40af; align-self: flex-start; }
.door-chat .dc-me { background: #3b82f6; align-self: flex-end; }
.door-chat .dc-home { width: 22px; height: 3px; background: rgba(255,255,255,.3); border-radius: 2px; flex-shrink: 0; }
.door-chat.stuck { filter: grayscale(1) brightness(.55); }
.door-chat.crescendo { transform: scale(1.35); box-shadow: 0 0 0 2px #3b82f6, 0 12px 48px rgba(29,78,216,.7), inset 0 1px 0 rgba(255,255,255,.25); }
/* Point */
.door-point { width: 88px; height: 124px; position: relative; display: flex; align-items: center; justify-content: center; transition: transform .25s cubic-bezier(.22,1,.36,1), filter .25s; }
.door-point svg { width: 88px; height: 124px; display: block; }
.door-point.stuck { filter: grayscale(1) brightness(.6); }
.door-point.crescendo { transform: scale(1.35); filter: drop-shadow(0 0 12px rgba(243,207,109,.6)) drop-shadow(0 0 32px rgba(138,90,43,.35)); }
#skip-banner h2 { margin: 0; font-size: 22px; }
#skip-code { font-family: ui-monospace, monospace; font-size: 12px; color: rgba(255,255,255,.35); margin-top: 4px; }
/* Final */
#finale { background: var(--c-bg); }
#finale h1 { margin: 0; font-size: 28px; }
#fin-stars { font-size: 26px; color: var(--c-gold); letter-spacing: 4px; }
#fin-word { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
#fin-word span {
width: 44px; height: 52px; background: var(--accent); border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards;
}
@keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
#fin-msg { color: rgba(255,255,255,.8); max-width: 56ch; }
/* Butoane */
.btn-main {
font: inherit; font-size: 16px; font-weight: 700;
background: var(--accent); color: #fff; border: none;
border-radius: 12px; padding: 14px 28px; cursor: pointer;
min-height: 44px; width: 100%; max-width: 320px;
}
.btn-main:hover { filter: brightness(1.1); }
.btn-main:disabled { opacity: .5; cursor: not-allowed; }
.btn-skip {
font: inherit; font-size: 15px; font-weight: 700;
background: #4b5563; color: #fff; border: none;
border-radius: 12px; padding: 12px 24px; cursor: pointer; min-height: 44px;
}
.confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 99; animation: fall linear forwards; }
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
@media (max-width: 599px) {
#chrome { height: 40px; min-height: 40px; }
#chrome-title { font-size: 13px; }
#dots span { width: 8px; height: 8px; }
}
</style>
</head>
<body>
<div id="chrome">
<span id="chrome-title">${esc(cfg.title)}</span>
<div class="sp"></div>
<div id="dots"></div>
</div>
<div id="room-wrap">
<iframe id="room-frame" data-room title="Camera curentă"></iframe>
<div id="intro" class="overlay show">
<h1 id="intro-title"></h1>
<p class="story-text" id="intro-story"></p>
<p class="promise" id="intro-promise"></p>
<button class="btn-main" id="btn-start">Începe aventura</button>
</div>
<div id="corridor" class="overlay">
<div id="corr-reward">
<div>
<div id="corr-label">Litera câștigată</div>
<div id="corr-letter"></div>
</div>
<div id="corr-stars"></div>
</div>
<div id="corr-door"></div>
<div id="corr-next"></div>
<button class="btn-main" id="btn-next">Deschide ușa →</button>
</div>
<div id="skip-banner" class="overlay">
<h2>⚠️ Ușa asta e înțepenită!</h2>
<div id="skip-door"></div>
<p>Camera nu a răspuns. Poți sări la cea următoare.</p>
<div class="skip-code" id="skip-code"></div>
<button class="btn-skip" id="btn-skip">Sari la camera următoare</button>
</div>
<div id="finale" class="overlay" data-final>
<h1>🏆 Evadare reușită!</h1>
<div class="fstars" id="fin-stars"></div>
<p>Cuvântul magic:</p>
<div id="fin-word"></div>
<p id="fin-msg"></p>
</div>
</div>
<script>
/*
* ASCII DIAGRAM — contractul parent.*:
* ┌──────────── orchestrator (window) ─────────────────────────┐
* │ nextRoom({idx,stars,letter}) ← camera.parent.nextRoom │
* │ roomReady(idx) ← camera.parent.roomReady │
* │ roomError(idx,msg) ← camera.parent.roomError │
* │ beep(ok) ← camera.parent.beep │
* └────────────────────────────────────────────────────────────┘
* Idempotență: nextRoom/roomError acceptate doar de la activeWindow.
* roomDone[idx]=true după primul nextRoom → duplicatele ignorate.
* Timeout 4s → skipRoom → aceeași compoziție de coridor (D5).
*/
var TPL = ${tplJson};
var MASTER = ${masterJson};
var ROTATION = ['classic','terminal','arcade','chat','point'];
var TOKEN = '__CFG__';
document.documentElement.style.setProperty('--accent', MASTER.color || '#6d28d9');
var N = MASTER.puzzles.length;
var totalStars = 0;
var collected = [];
var skipped = {};
var activeIdx = -1;
var activeWindow = null;
var readyTimer = null;
var roomDone = {};
/* ----- Resume — safeStore (D3) + djb2 hash (D11) ----- */
function djb2(s){
var h = 5381;
for(var i=0;i<s.length;i++) h = ((h<<5)+h) ^ s.charCodeAt(i);
return (h >>> 0).toString(36);
}
var _RESUME_KEY = 'esc-camp-' + djb2(JSON.stringify(MASTER));
function safeGet(){ try{ return JSON.parse(sessionStorage.getItem(_RESUME_KEY)); }catch(e){ return null; } }
function safeSet(v){ try{ sessionStorage.setItem(_RESUME_KEY, JSON.stringify(v)); }catch(e){ /* webview restrictat — continuă fără resume */ } }
function saveProgress(){
safeSet({ idx: activeIdx, totalStars: totalStars, collected: collected.slice(), skipped: skipped });
}
function clearProgress(){ try{ sessionStorage.removeItem(_RESUME_KEY); }catch(e){} }
var frameEl = document.getElementById('room-frame');
var introEl = document.getElementById('intro');
var corridorEl = document.getElementById('corridor');
var skipEl = document.getElementById('skip-banner');
var finaleEl = document.getElementById('finale');
/* ----- Dots ----- */
function buildDots(){
var d = document.getElementById('dots'); d.innerHTML = '';
for(var i=0;i<N;i++){
var s = document.createElement('span');
s.id = 'dot-'+i;
s.setAttribute('aria-label','Camera '+(i+1)+' din '+N);
d.appendChild(s);
}
}
function setDot(i,cls){ var d=document.getElementById('dot-'+i); if(d) d.className=cls; }
/* ----- Ușa coridorului (§Design pct.7) ----- */
function doorHtml(style, isLast, isStuck){
var cls = (isLast ? ' crescendo' : '') + (isStuck ? ' stuck' : '');
var lock = isStuck ? '<span class="door-lock">&#128274;</span>' : '';
if(style === 'classic') return '<div class="door-classic'+cls+'"><span class="dq">?</span>'+lock+'</div>';
if(style === 'terminal') return '<div class="door-terminal'+cls+'"><span class="dt-txt">'+(isStuck?'BLOCKED':'ACCESS')+'</span>'+(isStuck?'':' <span class="dt-cur">_</span>')+lock+'</div>';
if(style === 'arcade') return '<div class="door-arcade'+cls+'"><span class="da-sprite">&#9654;</span>'+lock+'</div>';
if(style === 'chat') return '<div class="door-chat'+cls+'"><div class="dc-notch"></div><div class="dc-screen"><div class="dc-bub dc-npc">Salut!</div><div class="dc-bub dc-me">?</div></div><div class="dc-home"></div>'+lock+'</div>';
if(style === 'point') return '<div class="door-point'+cls+'"><svg viewBox="0 0 88 124" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="84" height="120" rx="6" fill="#5a3518"/><rect x="6" y="6" width="76" height="112" rx="4" fill="#7c4f2c"/><rect x="10" y="10" width="30" height="44" rx="3" fill="rgba(0,0,0,.22)"/><rect x="48" y="10" width="30" height="44" rx="3" fill="rgba(0,0,0,.22)"/><rect x="10" y="62" width="68" height="52" rx="3" fill="rgba(0,0,0,.15)"/><circle cx="67" cy="70" r="8" fill="#c8952a"/><circle cx="67" cy="70" r="5" fill="#f3cf6d"/><circle cx="67" cy="70" r="2" fill="#a07022"/><circle cx="32" cy="87" r="13" fill="none" stroke="#f3cf6d" stroke-width="3"/><line x1="42" y1="97" x2="50" y2="105" stroke="#f3cf6d" stroke-width="3.5" stroke-linecap="round"/></svg>'+lock+'</div>';
return doorHtml('point', isLast, isStuck); /* fallback */
}
/* ----- Beep — singurul AudioContext (D2) ----- */
function beep(ok){
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){}
}
/* ----- parent.* API ----- */
window.nextRoom = function(data){
/* Guard: doar de la camera activă (D5) */
if(!activeWindow || frameEl.contentWindow !== activeWindow){
console.log('[campaign] nextRoom ignorat — frame stale'); return;
}
var idx = data ? +data.idx : activeIdx;
/* Idempotență (D4) */
if(roomDone[idx]){ console.log('[campaign] nextRoom ignorat — idx deja încheiat', idx); return; }
if(idx !== activeIdx){ console.log('[campaign] nextRoom idx mismatch ignorat'); return; }
clearTimeout(readyTimer);
roomDone[idx] = true;
totalStars += (data.stars || 0);
var letter = String(data.letter || '').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
if(letter) collected.push(letter);
setDot(idx,'done');
saveProgress();
console.log('[campaign] camera',idx,'done. stars=',data.stars,'letter=',letter);
var next = idx + 1;
if(next >= N){ clearProgress(); showFinale(); } else { showCorridor(idx, data, next); }
};
window.roomReady = function(idx){
console.log('[campaign] roomReady',idx);
if(+idx !== activeIdx) return;
clearTimeout(readyTimer);
frameEl.setAttribute('data-room-ready','true'); /* hook pentru tests */
};
window.roomError = function(idx, msg){
console.warn('[campaign] roomError',idx,msg);
/* roomError are semantică ORICÂND (post-ready inclus, D5) */
if(!activeWindow || frameEl.contentWindow !== activeWindow) return;
if(+idx !== activeIdx) return;
if(roomDone[idx]) return;
skipRoom(+idx, String(msg||'eroare'));
};
/* ----- Timeout 4s → skip (T3) ----- */
function startReadyTimer(idx){
clearTimeout(readyTimer);
readyTimer = setTimeout(function(){
if(roomDone[idx]) return;
console.warn('[campaign] timeout 4s — skip',idx);
skipRoom(idx,'timeout');
}, 4000);
}
function skipRoom(idx, reason){
clearTimeout(readyTimer);
roomDone[idx] = true;
skipped[idx] = true;
setDot(idx,'done');
saveProgress();
var style = (MASTER.puzzles[idx]&&(MASTER.puzzles[idx].style||ROTATION[idx%5]))||'?';
var code = style + '\\u00b7' + idx;
showSkipBanner(idx, code, reason);
}
/* ----- Montare cameră ----- */
function mountRoom(idx){
activeIdx = idx;
var puzzle = MASTER.puzzles[idx];
var style = (puzzle&&puzzle.style) || ROTATION[idx % ROTATION.length];
var tpl = TPL[style];
if(!tpl){
/* stil negăsit în template-uri — skip imediat */
console.warn('[campaign] template lipsă pentru stil',style);
skipRoom(idx,'template lipsă: '+style); return;
}
var camCfg = {
title: MASTER.title, player: MASTER.player, color: MASTER.color,
style: style, charName: MASTER.charName,
story: MASTER.story, finalMessage: MASTER.finalMessage,
puzzles: [puzzle],
_campaign: {
idx: idx, total: N,
stars: totalStars, letters: collected.slice(), deadline: null
}
};
/* json cu replace-funcție (D1 + D6) */
var json = JSON.stringify(camCfg).replace(/</g,'\\u003c');
var srcdoc = tpl.replace(TOKEN, function(){ return json; });
hideAll();
setDot(idx,'active');
activeWindow = null;
frameEl.removeAttribute('data-room-ready');
frameEl.srcdoc = srcdoc;
setTimeout(function(){ activeWindow = frameEl.contentWindow; }, 0);
startReadyTimer(idx);
var isLast = (idx === N - 1);
document.getElementById('chrome-title').textContent = isLast
? MASTER.title + ' — Ultima cameră!' : MASTER.title;
console.log('[campaign] montat camera',idx,'stil',style);
}
/* ----- Coridor ----- */
function showCorridor(doneIdx, data, nextIdx){
hideAll();
var s = data.stars || 0; var stars = '';
for(var i=0;i<s;i++) stars += '\\u2605';
document.getElementById('corr-stars').textContent = stars || '\\u2606';
var letter = String(data.letter||'').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
document.getElementById('corr-letter').textContent = letter || '\\u2014';
var styleNames = {classic:'Clasic',terminal:'Terminal Retro',arcade:'Arcade Pixel',chat:'Story Chat',point:'Point-and-Click'};
var nextStyle = (MASTER.puzzles[nextIdx] && (MASTER.puzzles[nextIdx].style || ROTATION[nextIdx%5])) || 'classic';
var isLast = (nextIdx === N - 1);
document.getElementById('corr-next').textContent =
isLast ? '\\u2605 Ultima cameră!' : 'Camera '+(nextIdx+1)+' — '+styleNames[nextStyle];
/* Ușa ca erou (§Design pct.2 + pct.6) */
var doorEl = document.getElementById('corr-door');
doorEl.innerHTML = doorHtml(nextStyle, isLast, false);
corridorEl.classList.add('show');
var btn = document.getElementById('btn-next');
btn.disabled = false;
btn.onclick = function(){
btn.disabled = true; /* idempotență buton (T4) */
/* Animație deschidere ușă ~250ms (§Design pct.4) */
var d = doorEl.firstElementChild;
if(d) d.classList.add('opening');
setTimeout(function(){ mountRoom(nextIdx); }, 280);
};
}
/* ----- Skip banner ----- */
function showSkipBanner(idx, code, reason){
hideAll();
var stuckStyle = (MASTER.puzzles[idx] && (MASTER.puzzles[idx].style || ROTATION[idx%5])) || 'classic';
document.getElementById('skip-door').innerHTML = doorHtml(stuckStyle, false, true);
document.getElementById('skip-code').textContent = 'Cod: ' + code + ' (' + reason + ')';
skipEl.classList.add('show');
var next = idx + 1;
var btn = document.getElementById('btn-skip');
btn.disabled = false;
btn.onclick = function(){
btn.disabled = true;
if(next >= N){ showFinale(); } else { showCorridor(idx,{stars:0,letter:''},next); }
};
}
/* ----- Final ----- */
function showFinale(){
hideAll(); finaleEl.classList.add('show');
var wEl = document.getElementById('fin-word'); wEl.innerHTML = '';
collected.forEach(function(l,j){
var s = document.createElement('span');
s.textContent = l;
s.style.animationDelay = (j*0.18)+'s';
wEl.appendChild(s);
});
/* dăle-lacăt pentru camere sărite */
Object.keys(skipped).forEach(function(i){
var s = document.createElement('span');
s.textContent = '\\uD83D\\uDD12'; /* 🔒 */
s.title = 'Camera '+(+i+1)+' sărită';
s.style.fontSize = '22px';
wEl.appendChild(s);
});
document.getElementById('fin-stars').textContent = totalStars + ' / ' + (N*3) + ' \\u2605';
var msg = MASTER.finalMessage || '';
var pl = MASTER.player || '';
document.getElementById('fin-msg').textContent = pl ? pl+', '+msg.charAt(0).toLowerCase()+msg.slice(1) : msg;
beep(true); confetti();
}
/* ----- Confetti ----- */
function confetti(){
var colors=[MASTER.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 hideAll(){
[introEl,corridorEl,skipEl,finaleEl].forEach(function(el){ el.classList.remove('show'); });
}
/* ----- Intro ----- */
document.getElementById('intro-title').textContent = MASTER.title;
document.getElementById('intro-story').textContent = (MASTER.player?'Salut, '+MASTER.player+'! ':'')+MASTER.story;
document.getElementById('intro-promise').textContent = N+' camere \\u00b7 ${nStyles} stiluri \\u00b7 1 cuvânt magic';
document.getElementById('btn-start').onclick = function(){
/* Deblochează AudioContext-ul AICI (gest direct pe părinte) — camerele cheamă
parent.beep() din iframe, iar gestul din iframe NU deblochează ctx-ul părintelui. */
try{ var c=beep._ctx||(beep._ctx=new(window.AudioContext||window.webkitAudioContext)()); if(c.state==='suspended') c.resume(); }catch(e){}
clearProgress(); mountRoom(0);
};
buildDots();
/* ----- Resume la reload (D11) ----- */
(function tryResume(){
if(MASTER._noResume) return; /* preview-ul nu reia niciodată */
var saved = safeGet();
if(!saved || typeof saved.idx !== 'number' || saved.idx < 0) return;
/* restaurăm starea */
totalStars = saved.totalStars || 0;
collected = saved.collected || [];
skipped = saved.skipped || {};
Object.keys(skipped).forEach(function(k){ roomDone[+k] = true; setDot(+k,'done'); });
/* repornim de la coridorul camerei next */
var resumeIdx = saved.idx + 1;
if(resumeIdx >= N){
/* ultima cameră deja terminată — mergi direct la final */
showFinale(); return;
}
showCorridor(saved.idx, {stars:0, letter: (collected[collected.length-1]||'')}, resumeIdx);
})();
<\/script>
</body>
</html>`;
}
/* ---------- start ---------- */
renderGlobals();
renderPuzzles();
refreshPreview();
</script>
</body>
</html>