Files
escape-builder/escape-builder.html
Claude Agent ba949f43b0 bomberman: sunete (sfx), raza initiala 1, powerup-uri raza/bombe
Feedback user: nu se aud sunete, raza prea mare, lipsesc powerup-urile.

- sfx(type) WebAudio local in arcade: bomb/explosion/enemy/powerup/death;
  beep(ok) din libJS ramane pentru raspuns corect/gresit.
- raza fixa EXPLOSION_RANGE=3 -> bombRange variabil de la BASE_RANGE=1
  (Bomberman clasic); maxBombs de la BASE_BOMBS=1.
- powerup-uri: la spargerea cutiei, sansa 0.32 sa cada flacara (raza+1)
  sau bomba (bombe+1); ridicate mergand pe ele; HUD arata bombe/raza.
- fix: powerup-ul cadea pe celula cutiei si checkExplosionHits il stergea
  instant -> colectez brokenBoxes, drop dupa checkExplosionHits.

Hooks __game: powerups/bombRange/maxBombs/dropPowerupAt. Smoke 27/27.

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

2374 lines
127 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.ck { display: flex; align-items: flex-start; gap: 7px; cursor: pointer; line-height: 1.4; }
label.ck input { width: auto; margin-top: 1px; flex-shrink: 0; }
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 class="ck"><input type="checkbox" id="gVoice" data-gb="voice"> Naratiune vocala &mdash; citeste povestea si intrebarile cu vocea sistemului (doar in Campanie multi-stil)</label>
<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',
voice: false,
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]; });
document.querySelectorAll('[data-gb]').forEach(el => { el.checked = !!state[el.dataset.gb]; });
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();
});
});
document.querySelectorAll('[data-gb]').forEach(el => {
el.addEventListener('change', () => { state[el.dataset.gb] = el.checked; 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: #f1f0ff; display: flex; align-items: center; justify-content: center; padding: 16px;
background: radial-gradient(ellipse at 50% 30%, #2a0e5e 0%, #0e0622 70%);
}
.card {
width: 100%; max-width: 560px; background: #1a0e3d;
border: 1px solid rgba(255,255,255,.18); border-radius: 20px; padding: 26px;
backdrop-filter: blur(6px);
box-shadow: 0 0 0 1px rgba(255,255,255,.06), 0 24px 60px rgba(0,0,0,.55), 0 0 40px rgba(109,40,217,.35);
}
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 cubic-bezier(.22,1,.36,1); }
@keyframes pop { from { transform: scale(.94) translateY(6px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }
.progress { height: 10px; background: rgba(255,255,255,.12); border-radius: 99px; overflow: hidden; margin: 14px 0 4px; }
.progress i { display: block; height: 100%; background: var(--accent); width: 0; box-shadow: 0 0 8px var(--accent); transition: width .5s cubic-bezier(.22,1,.36,1); }
.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: 44px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center;
font-weight: 800; font-size: 20px; 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; box-shadow: 0 0 12px var(--accent); animation: flip .5s cubic-bezier(.34,1.56,.64,1); }
@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: 21px; line-height: 1.5; margin: 8px 0 18px; color: #f1f0ff; }
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); outline-offset: 2px; 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; min-height: 44px;
}
button:hover { filter: brightness(1.12); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button.opt { background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.16); font-weight: 600; text-align: left; min-height: 48px; transition: background .15s, border-color .15s; }
button.opt:hover { background: rgba(255,255,255,.16); border-color: var(--accent); }
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); } }
@media (prefers-reduced-motion: reduce) {
.screen.on, .tile.won, .bigword span, .shake { animation: none; }
.confetti { display: none !important; }
.progress i { transition: none; }
}
</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){} } }
/* Contract de finalizare a camerei — un singur loc pentru payload-ul parent.nextRoom
(înlocuiește duplicatele din showFinal/finale; D7). Citește totalStars + finalWord() la apel. */
function campaignDone(){ if(CFG._campaign){ try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:finalWord().charAt(0)}); }catch(e){} } }
window.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } };
if(CFG._campaign){
/* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */
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); } }
@media (prefers-reduced-motion: reduce){ .confetti{ display:none !important; } .shake{ animation:none !important; } }`;
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); } }
@media (prefers-reduced-motion: reduce){ #fOverlay .fword span{ animation:none !important; } }
#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){ campaignDone(); return; }
el('fStars').textContent = totalStars + ' / ' + (CFG.puzzles.length * 3) + ' \\u2605';
var w = finalWord(); var bw = el('fWord'); bw.innerHTML = '';
for (var j = 0; j < w.length; j++){ var s = document.createElement('span'); s.textContent = w.charAt(j); s.style.animationDelay = (j * 0.18) + 's'; bw.appendChild(s); }
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: #040f08; color: #39ff6e; font-family: "Courier New", ui-monospace, monospace; animation: crt-flicker 6s infinite; }
@keyframes crt-flicker { 0%,96%,100% { opacity: 1; } 97% { opacity: 1; } 98% { opacity: .94; } 99% { opacity: .98; } }
#crt { max-width: 680px; 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 8px rgba(57,255,110,.55); }
.line.dim { color: #2ecc71; }
.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; min-height: 44px; text-shadow: 0 0 8px rgba(57,255,110,.55); }
#cmd { flex: 1; min-height: 44px; background: none; border: none; outline: none; color: inherit; font: inherit; text-shadow: inherit; caret-color: #39ff6e; }
.scan { position: fixed; inset: 0; pointer-events: none; z-index: 2; background: repeating-linear-gradient(0deg, rgba(0,0,0,.22) 0 1px, transparent 1px 3px); }
.vign { position: fixed; inset: 0; pointer-events: none; z-index: 2; background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.6)); }
#crt-frame { position: fixed; inset: 0; pointer-events: none; z-index: 3; border: 8px solid #0d1f12; border-radius: 18px; box-shadow: inset 0 0 60px rgba(0,0,0,.6), inset 0 0 0 1px #1a3a24; }
@media (prefers-reduced-motion: reduce) { body { animation: none; } }
</style>
</head>
<body>
<div class="scan"></div><div class="vign"></div><div id="crt-frame"></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', campaignDone);
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: radial-gradient(ellipse at 50% 0%, #1a0a40 0%, #080614 60%); color: #fff; font-family: ui-monospace, "Courier New", monospace; display: flex; flex-direction: column; align-items: center; }
h1 { font-size: 22px; margin: 12px 0 4px; letter-spacing: .12em; text-transform: uppercase; color: #fff; text-shadow: 0 0 12px var(--accent), 0 0 24px rgba(109,40,217,.5); }
#hud { display: flex; gap: 16px; align-items: center; font-size: 13px; color: #c4b5fd; margin-bottom: 8px; flex-wrap: wrap; justify-content: center; padding: 0 10px; }
#hudLetters { display: flex; gap: 4px; }
#hudLetters span { width: 32px; height: 32px; border-radius: 4px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); display: flex; align-items: center; justify-content: center; font-weight: 800; color: rgba(255,255,255,.4); font-size: 14px; }
#hudLetters span.won { background: var(--accent); color: #fff; border-color: transparent; box-shadow: 0 0 8px var(--accent); }
canvas { border: 4px solid var(--accent); border-radius: 4px; background: #0e0a22; max-width: calc(100vw - 16px); image-rendering: pixelated; box-shadow: 0 0 0 2px #080614, 0 0 20px rgba(109,40,217,.6), 0 0 40px rgba(109,40,217,.25), inset 0 0 30px rgba(0,0,0,.6); }
.help { font-size: 12px; color: #8b7fc0; 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: 56px; height: 52px; font-size: 20px; border-radius: 6px; border: 2px solid #6d28d9; background: #1a1040; color: #c4b5fd; cursor: pointer; box-shadow: 0 4px 0 #0d0820, 0 0 8px rgba(109,40,217,.3); transition: transform .08s, box-shadow .08s; }
#dpad button:active { background: var(--accent); transform: translateY(2px); box-shadow: 0 2px 0 #0d0820, 0 0 12px var(--accent); }
#btnBomb { background: #7f1d1d; border-color: #b91c1c; }
@media (prefers-reduced-motion: reduce) { #dpad button { transition: none; } }
#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 (uneori cad bonusuri: &#128293; raza, &#128163; bombe in plus), evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.</div>
<div id="dpad"><button data-d="L" aria-label="Stanga">&#9664;</button><button data-d="U" aria-label="Sus">&#9650;</button><button data-d="D" aria-label="Jos">&#9660;</button><button data-d="R" aria-label="Dreapta">&#9654;</button><button id="btnBomb" aria-label="Pune bomba">&#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);
/* ----- Efecte sonore arcade (WebAudio local; deblocat de gesturile din iframe) -----
beep(ok) din libJS ramane pentru raspuns corect/gresit; sfx() adauga bomba/explozie/powerup. */
function sfx(type){
try {
var actx = sfx.ctx || (sfx.ctx = new (window.AudioContext || window.webkitAudioContext)());
if (actx.state === 'suspended') actx.resume();
var t = actx.currentTime;
function tone(wave, f0, f1, dur, vol){ var o = actx.createOscillator(), g = actx.createGain(); o.type = wave; o.frequency.setValueAtTime(f0, t); if (f1 !== f0) o.frequency.exponentialRampToValueAtTime(f1, t + dur); g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.0008, t + dur); o.connect(g); g.connect(actx.destination); o.start(t); o.stop(t + dur + 0.02); }
if (type === 'bomb'){ tone('square', 440, 150, 0.1, 0.07); }
else if (type === 'explosion'){
var dur = 0.45, sr = actx.sampleRate, buf = actx.createBuffer(1, Math.floor(sr * dur), sr), data = buf.getChannelData(0);
for (var i = 0; i < data.length; i++){ var k = 1 - i / data.length; data[i] = (Math.random() * 2 - 1) * k * k; }
var src = actx.createBufferSource(); src.buffer = buf;
var lp = actx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(1100, t); lp.frequency.exponentialRampToValueAtTime(180, t + dur);
var g = actx.createGain(); g.gain.setValueAtTime(0.38, t); g.gain.exponentialRampToValueAtTime(0.0008, t + dur);
src.connect(lp); lp.connect(g); g.connect(actx.destination); src.start(t);
tone('sine', 130, 42, 0.34, 0.3);
}
else if (type === 'enemy'){ tone('square', 200, 520, 0.14, 0.08); }
else if (type === 'powerup'){ var fs = [523, 659, 784, 1047]; for (var p = 0; p < fs.length; p++){ var o = actx.createOscillator(), gg = actx.createGain(); o.type = 'triangle'; o.frequency.value = fs[p]; gg.gain.setValueAtTime(0.08, t + p * 0.06); gg.gain.exponentialRampToValueAtTime(0.0008, t + p * 0.06 + 0.13); o.connect(gg); gg.connect(actx.destination); o.start(t + p * 0.06); o.stop(t + p * 0.06 + 0.15); } }
else if (type === 'death'){ tone('sawtooth', 330, 55, 0.5, 0.12); }
} catch (e) {}
}
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, BASE_RANGE = 1, BASE_BOMBS = 1, POWERUP_CHANCE = 0.32, 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 P_RANGE = 'range', P_BOMB = 'bomb';
var map, player, enemies, bombs, explosions, lives, gameOver, gameWon, puzzleProgress, doorMeta, chestPos, powerups, bombRange, maxBombs;
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 = []; powerups = []; bombRange = BASE_RANGE; maxBombs = BASE_BOMBS; 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 + ' \\ud83d\\udca3' + (maxBombs || 1) + ' \\ud83d\\udd25' + (bombRange || 1);
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 >= maxBombs) return;
for (var i = 0; i < bombs.length; i++) if (bombs[i].x === player.x && bombs[i].y === player.y) return;
bombs.push({ x: player.x, y: player.y, timer: BOMB_TIMER, id: bombIdCounter++ });
sfx('bomb');
updateHud();
}
function explodeBomb(bomb){
bombs = bombs.filter(function(b){ return b.id !== bomb.id; });
var cells = [{ x: bomb.x, y: bomb.y }];
var brokenBoxes = [];
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 <= bombRange; 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; brokenBoxes.push({ x: cx, y: cy }); break; } if (t === T_DOOR || t === T_CHEST) break; } }
explosions.push({ cells: cells, endTime: performance.now() + EXPLOSION_TIME });
sfx('explosion');
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);
/* drop DUPA checkExplosionHits: altfel powerup-ul de pe celula cutiei e sters instant de filtrul de explozie */
for (var bx = 0; bx < brokenBoxes.length; bx++) maybeDropPowerup(brokenBoxes[bx].x, brokenBoxes[bx].y);
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; sfx('enemy'); }
powerups = powerups.filter(function(p){ return !(p.x === cx && p.y === cy); });
if (player.alive && !player.invincible && player.x === cx && player.y === cy) killPlayer();
}
}
function maybeDropPowerup(x, y){
if (rng() >= POWERUP_CHANCE) return;
powerups.push({ x: x, y: y, type: rng() < 0.5 ? P_RANGE : P_BOMB });
}
function pickupPowerup(){
for (var i = 0; i < powerups.length; i++) if (powerups[i].x === player.x && powerups[i].y === player.y){
if (powerups[i].type === P_RANGE) bombRange++; else maxBombs++;
powerups.splice(i, 1); sfx('powerup'); updateHud(); return;
}
}
function killPlayer(){
if (!player.alive) return;
player.alive = false; lives--; sfx('death'); 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;
pickupPowerup();
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 pu = 0; pu < powerups.length; pu++) drawPowerup(powerups[pu], now);
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 drawPowerup(p, now){ var px = p.x * TS, py = p.y * TS, cx = px + TS/2, cy = py + TS/2, pulse = Math.sin(now / 200) * 0.12 + 0.88; var isR = p.type === P_RANGE; ctx.fillStyle = isR ? 'rgba(249,115,22,.25)' : 'rgba(59,130,246,.25)'; ctx.fillRect(px + 3, py + 3, TS - 6, TS - 6); ctx.fillStyle = isR ? '#f97316' : '#3b82f6'; ctx.beginPath(); ctx.arc(cx, cy, (TS/2 - 6) * pulse, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#fff'; if (isR){ ctx.beginPath(); ctx.moveTo(cx, cy - 7); ctx.lineTo(cx + 5, cy + 6); ctx.lineTo(cx, cy + 2); ctx.lineTo(cx - 5, cy + 6); ctx.closePath(); ctx.fill(); } else { ctx.beginPath(); ctx.arc(cx, cy + 1, 5, 0, Math.PI*2); ctx.fill(); ctx.fillRect(cx - 1, cy - 8, 2, 4); } }
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 powerups(){ return powerups ? powerups.slice() : []; },
get bombRange(){ return bombRange; },
get maxBombs(){ return maxBombs; },
dropPowerupAt: function(x, y, type){ powerups.push({ x: x, y: y, type: type || P_RANGE }); },
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: #060d1a; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #e2e8f0; display: flex; justify-content: center; min-height: 100vh; }
#app { width: 100%; max-width: 480px; height: 100vh; height: 100dvh; display: flex; flex-direction: column; background: #0d1626; }
header { display: flex; gap: 10px; align-items: center; padding: 10px 14px; background: rgba(23,32,53,.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid rgba(255,255,255,.08); box-shadow: 0 1px 0 rgba(255,255,255,.05); }
.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; box-shadow: 0 0 0 2px rgba(255,255,255,.15); }
.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 cubic-bezier(.22,1,.36,1); }
@keyframes bin { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } }
.row.him .bub { background: #1e2d45; border: 1px solid rgba(255,255,255,.08); box-shadow: 0 2px 8px rgba(0,0,0,.25); color: #e2e8f0; border-bottom-left-radius: 5px; }
.row.me .bub { background: var(--accent); color: #fff; box-shadow: 0 2px 12px rgba(109,40,217,.4); border-bottom-right-radius: 5px; }
.bub.tile { font-size: 28px; font-weight: 900; letter-spacing: 3px; padding: 14px 20px; background: linear-gradient(135deg, #14532d, #166534); border: 1px solid #22c55e; box-shadow: 0 0 16px rgba(34,197,94,.3); animation: tile-pop .4s cubic-bezier(.34,1.56,.64,1); }
@keyframes tile-pop { from { transform: scale(.6) rotate(-5deg); opacity: 0; } to { transform: none; opacity: 1; } }
.bub.typing i { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #64748b; 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(-6px); background: #34d399; } }
#composer { padding: 10px 12px; background: rgba(23,32,53,.9); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border-top: 1px solid rgba(255,255,255,.08); display: flex; flex-wrap: wrap; gap: 8px; min-height: 58px; }
#composer input { flex: 1; min-width: 120px; min-height: 44px; font: inherit; font-size: 15px; padding: 9px 13px; border-radius: 99px; border: 1px solid #334155; background: rgba(13,22,38,.8); color: #fff; }
#composer input:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-color: transparent; }
#composer button { font: inherit; cursor: pointer; border: none; border-radius: 99px; padding: 9px 16px; min-height: 44px; min-width: 44px; font-weight: 600; background: var(--accent); color: #fff; }
#composer button.chip { background: #0d1626; border: 1px solid #334155; color: #cbd5e1; min-height: 44px; }
#composer button.chip:hover { border-color: var(--accent); color: #fff; }
@media (prefers-reduced-motion: reduce) { .bub, .bub.tile, .bub.typing i { animation: none; } }
${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: linear-gradient(180deg, #0a0618 0%, #150d30 100%); color: #fff; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; display: flex; flex-direction: column; align-items: center; }
h1 { font-size: 20px; margin: 14px 0 4px; color: #e8deff; letter-spacing: .04em; text-shadow: 0 2px 8px rgba(0,0,0,.6); }
#hud { display: flex; gap: 16px; align-items: center; font-size: 13px; color: #d4c8f8; margin-bottom: 4px; flex-wrap: wrap; justify-content: center; padding: 0 10px; }
#hudLetters { display: flex; gap: 4px; }
#hudLetters span { width: 26px; height: 30px; border-radius: 6px; background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.18); 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; box-shadow: 0 0 10px var(--accent); }
.note { font-size: 13px; color: #a89fd4; 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.5) drop-shadow(0 0 8px rgba(255,220,100,.5)); transition: filter .15s; }
.hot.done { opacity: .85; cursor: default; }
.hot.done:hover { filter: none; }
#door { cursor: pointer; }
#door.open { filter: drop-shadow(0 0 18px rgba(34,197,94,.5)) drop-shadow(0 0 6px #fff); animation: door-glow 2s ease-in-out infinite alternate; }
@keyframes door-glow { from { filter: drop-shadow(0 0 12px rgba(34,197,94,.5)); } to { filter: drop-shadow(0 0 24px rgba(34,197,94,.5)) drop-shadow(0 0 8px rgba(255,255,255,.3)); } }
@media (prefers-reduced-motion: reduce) { .hot { transition: none; } #door.open { animation: none; filter: drop-shadow(0 0 18px rgba(34,197,94,.5)); } }
${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; }
#btn-voice {
width: 34px; height: 34px; min-width: 34px; padding: 0; border: 0; cursor: pointer;
border-radius: 8px; background: rgba(255,255,255,.12); color: #fff;
font-size: 17px; line-height: 1; display: inline-flex; align-items: center; justify-content: center;
}
#btn-voice[hidden] { display: none; } /* id batea specificitatea UA [hidden] */
#btn-voice:hover { background: rgba(255,255,255,.22); }
#btn-voice[aria-pressed="false"] { opacity: .5; }
#btn-voice:focus-visible { outline: 2px solid #a78bfa; outline-offset: 2px; }
#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; }
/* ===== Overworld (hartă top-down — înlocuiește coridorul) ===== */
#overworld.overlay { padding: 0; gap: 0; background: var(--c-bg); }
#ow-wrap { position: relative; flex: 1; width: 100%; overflow: hidden; }
#ow-world { position: absolute; left: 0; top: 0; transition: transform .12s linear; }
.ow-tile { position: absolute; width: 40px; height: 40px; }
.ow-floor { background: #2a1d4d; }
.ow-floor.alt { background: #2f2156; }
.ow-wall { background: #14092e; box-shadow: inset 0 0 0 1px rgba(0,0,0,.35); }
.ow-door { position: absolute; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 15px; color: #fff; border-radius: 7px; background: #e11d48; box-shadow: 0 2px 8px rgba(0,0,0,.5); }
.ow-door.solved { background: var(--c-gold); color: #3a2606; }
.ow-door.target { box-shadow: 0 0 0 3px #a78bfa, 0 2px 10px rgba(167,139,250,.6); }
.ow-exit { position: absolute; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; font-size: 20px; border-radius: 7px; background: #3b2a63; filter: grayscale(1) brightness(.7); }
.ow-exit.open { background: #166534; filter: none; box-shadow: 0 0 14px #22c55e; }
.ow-player { position: absolute; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; font-size: 24px; transition: left .1s linear, top .1s linear; z-index: 3; }
#ow-hint { position: absolute; left: 0; right: 0; bottom: 8px; text-align: center; font-size: 13px; color: rgba(255,255,255,.72); z-index: 4; pointer-events: none; padding: 0 8px; }
#ow-toast { position: absolute; left: 50%; top: 10px; transform: translateX(-50%); background: rgba(0,0,0,.72); padding: 6px 14px; border-radius: 20px; font-size: 14px; font-weight: 700; color: var(--c-gold); z-index: 4; opacity: 0; transition: opacity .3s; pointer-events: none; }
#ow-toast.show { opacity: 1; }
#ow-dpad { position: absolute; right: 10px; bottom: 10px; display: grid; grid-template-columns: repeat(3, 44px); grid-template-rows: repeat(3, 44px); gap: 4px; z-index: 5; }
#ow-dpad button { border: 1px solid #4a3590; background: rgba(34,22,67,.85); color: #cdc3f0; border-radius: 9px; font-size: 16px; cursor: pointer; }
#ow-dpad button:active { background: var(--accent); }
#ow-dpad .sp { visibility: hidden; }
/* Skip */
#skip-banner { background: var(--c-bg); }
/* ===== UȘILE — 5 stiluri × 3 stări ===== */
: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; }
#fin-word span { animation: none !important; }
.door-terminal .dt-cur { animation: none !important; }
.confetti { display: none !important; }
}
/* 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>
<button id="btn-voice" type="button" aria-label="Naratiune vocala" hidden>&#128266;</button>
<div id="dots" role="group" aria-label="Progres camere"></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="overworld" class="overlay">
<div id="ow-wrap">
<div id="ow-world"></div>
<div id="ow-toast"></div>
<div id="ow-hint"></div>
<div id="ow-dpad" role="group" aria-label="Deplasare pe harta">
<button class="sp" aria-hidden="true" tabindex="-1"></button><button data-d="U" aria-label="Sus">&#9650;</button><button class="sp" aria-hidden="true" tabindex="-1"></button>
<button data-d="L" aria-label="Stanga">&#9664;</button><button class="sp" aria-hidden="true" tabindex="-1"></button><button data-d="R" aria-label="Dreapta">&#9654;</button>
<button class="sp" aria-hidden="true" tabindex="-1"></button><button data-d="D" aria-label="Jos">&#9660;</button><button class="sp" aria-hidden="true" tabindex="-1"></button>
</div>
</div>
</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 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('role','img');
d.appendChild(s);
setDot(i,''); /* setează aria-label inițial (neînceput) */
}
}
function setDot(i,cls){
var d=document.getElementById('dot-'+i); if(!d) return;
d.className=cls;
var st = cls==='done' ? 'rezolvata' : (cls==='active' ? 'in curs' : 'neinceputa');
d.setAttribute('aria-label','Camera '+(i+1)+' din '+N+': '+st);
}
/* ----- 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)());
if(ctx.state==='suspended') ctx.resume(); /* safety: ctx poate fi suspendat din nou */
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){}
}
/* ----- Deblocare audio (D2) — primul gest pe părinte creează+deblochează ctx-ul.
Necesar pe TOATE căile, nu doar btn-start: la resume (reload mid-campanie) se intră
direct pe hartă fără btn-start, iar camerele cheamă parent.beep() din iframe (gestul
din iframe NU deblochează ctx-ul părintelui). Pe iOS Safari resume() singur nu ajunge
→ redăm și un buffer silențios în gest. Listener one-time, se auto-elimină. */
function unlockAudio(){
try{
var c=beep._ctx||(beep._ctx=new(window.AudioContext||window.webkitAudioContext)());
if(c.state==='suspended') c.resume();
var b=c.createBuffer(1,1,22050),s=c.createBufferSource();
s.buffer=b; s.connect(c.destination); s.start(0);
}catch(e){}
}
var _audioUnlocked=false;
function _onFirstGesture(){
if(_audioUnlocked) return; _audioUnlocked=true; unlockAudio();
document.removeEventListener('pointerdown',_onFirstGesture,true);
document.removeEventListener('keydown',_onFirstGesture,true);
}
document.addEventListener('pointerdown',_onFirstGesture,true);
document.addEventListener('keydown',_onFirstGesture,true);
/* ----- Narațiune vocală (D10) — opt-in via MASTER.voice, buton în bara chrome.
Edge cases tratate: (1) getVoices() poate fi gol sincron → re-citim la voiceschanged;
(2) fără voce ro-* → vocea default a sistemului (nu setăm u.voice); (3) la fiecare
schimbare de scenă (hideAll) → speechSynthesis.cancel() (fără replici fantomă);
(4) fără API → butonul rămâne ascuns, totul devine no-op. */
var SPEECH = ('speechSynthesis' in window) && !!MASTER.voice;
var voiceOn = SPEECH; /* pornit implicit când feature-ul e activat din builder */
var _roVoice = null, _voicesReady = false;
function _pickVoice(){
try{
var vs = window.speechSynthesis.getVoices();
if(!vs || !vs.length) return; /* gol sincron — așteptăm voiceschanged */
_voicesReady = true;
_roVoice = vs.filter(function(v){ return /(^|[^a-z])ro([-_]|$)/i.test(v.lang||''); })[0] || null;
}catch(e){}
}
if(SPEECH){
_pickVoice();
try{ window.speechSynthesis.onvoiceschanged = _pickVoice; }catch(e){}
}
function voiceCancel(){ if(SPEECH){ try{ window.speechSynthesis.cancel(); }catch(e){} } }
function voiceSay(text){
if(!SPEECH || !voiceOn || !text) return;
try{
window.speechSynthesis.cancel();
if(!_voicesReady) _pickVoice();
var u = new SpeechSynthesisUtterance(String(text));
if(_roVoice){ u.voice = _roVoice; u.lang = _roVoice.lang; } else { u.lang = 'ro-RO'; }
u.rate = 1; u.pitch = 1;
window.speechSynthesis.speak(u);
}catch(e){}
}
window.voiceSay = voiceSay; /* expus camerelor: parent.voiceSay(replica) — guard în motoare */
/* ----- 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 { showOverworld(next, data); }
};
window.roomReady = function(idx){
console.log('[campaign] roomReady',idx);
if(+idx !== activeIdx) return;
clearTimeout(readyTimer);
frameEl.setAttribute('data-room-ready','true'); /* hook pentru tests */
var q = MASTER.puzzles[idx] && MASTER.puzzles[idx].question;
if(q) voiceSay(q); /* citește întrebarea camerei (D10) */
};
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);
}
/* ----- 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 { showOverworld(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();
voiceSay(document.getElementById('fin-msg').textContent); /* citește mesajul final (D10) */
}
/* ----- 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);
}
}
var overworldEl = document.getElementById('overworld');
function hideAll(){
voiceCancel(); /* fără replici fantomă la schimbarea scenei (D10) */
[introEl,overworldEl,skipEl,finaleEl].forEach(function(el){ el.classList.remove('show'); });
}
/* ===== Overworld (S3 pas2 — hartă top-down care înlocuiește coridorul) =====
* Strat de NAVIGARE peste #room-frame. Nu schimbă contractul:
* mountRoom/nextRoom/roomReady/roomError/skip/resume/finale rămân identice.
* Camera done → showOverworld(next) (în loc de showCorridor). */
var OW_TILE = 40;
var OW_ROWS = 9;
var OW_COLS = Math.max(11, Math.min(19, N * 2 + 5));
var OW_MIDR = OW_ROWS >> 1;
var owWorld = document.getElementById('ow-world');
var owWrap = document.getElementById('ow-wrap');
var owMap = [], owDoors = [], owExit = { col: OW_COLS - 2, row: OW_MIDR };
var owPlayer = { col: 1, row: OW_MIDR }, owPlayerEl = null, owTargetIdx = 0, owActive = false;
function owResetPlayer(){ owPlayer.col = 1; owPlayer.row = OW_MIDR; }
function owBuild(){
owMap = [];
for (var r = 0; r < OW_ROWS; r++){ owMap[r] = []; for (var c = 0; c < OW_COLS; c++){ owMap[r][c] = (r === 0 || c === 0 || r === OW_ROWS - 1 || c === OW_COLS - 1) ? 1 : 0; } }
owDoors = [];
for (var i = 0; i < N; i++){
var col = (N <= 1) ? (OW_COLS >> 1) : (3 + Math.round(i * (OW_COLS - 6) / (N - 1)));
var row = OW_MIDR + ((i % 2 === 0) ? -1 : 1) * ((i % 4 < 2) ? 1 : 2);
if (row < 1) row = 1; if (row > OW_ROWS - 2) row = OW_ROWS - 2;
owDoors.push({ col: col, row: row, idx: i });
}
owWorld.style.width = (OW_COLS * OW_TILE) + 'px';
owWorld.style.height = (OW_ROWS * OW_TILE) + 'px';
var html = '';
for (var r2 = 0; r2 < OW_ROWS; r2++) for (var c2 = 0; c2 < OW_COLS; c2++){
var cls = owMap[r2][c2] === 1 ? 'ow-wall' : ('ow-floor' + (((r2 + c2) % 2) ? ' alt' : ''));
html += '<div class="ow-tile ' + cls + '" style="left:' + (c2 * OW_TILE) + 'px;top:' + (r2 * OW_TILE) + 'px"></div>';
}
owDoors.forEach(function(d){ html += '<div class="ow-door" id="ow-door-' + d.idx + '" style="left:' + (d.col * OW_TILE) + 'px;top:' + (d.row * OW_TILE) + 'px">' + (d.idx + 1) + '</div>'; });
html += '<div class="ow-exit" id="ow-exit" style="left:' + (owExit.col * OW_TILE) + 'px;top:' + (owExit.row * OW_TILE) + 'px">\\ud83c\\udfc1</div>';
html += '<div class="ow-player" id="ow-player" style="left:' + (owPlayer.col * OW_TILE) + 'px;top:' + (owPlayer.row * OW_TILE) + 'px">\\ud83e\\uddd1</div>';
owWorld.innerHTML = html;
owPlayerEl = document.getElementById('ow-player');
}
function owAllDone(){ for (var i = 0; i < N; i++) if (!roomDone[i]) return false; return true; }
function owRefreshDoors(){
owDoors.forEach(function(d){
var el = document.getElementById('ow-door-' + d.idx); if (!el) return;
var done = !!roomDone[d.idx], isSkip = !!skipped[d.idx];
el.className = 'ow-door' + (done ? ' solved' : '') + (!done && d.idx === owTargetIdx ? ' target' : '');
if (isSkip) el.textContent = '\\ud83d\\udd12';
else if (done) el.textContent = (MASTER.puzzles[d.idx].letter || '').trim().toUpperCase() || '\\u2713';
else el.textContent = (d.idx + 1);
});
var ex = document.getElementById('ow-exit'); if (ex) ex.className = 'ow-exit' + (owAllDone() ? ' open' : '');
document.getElementById('ow-hint').textContent = owAllDone()
? 'Toate camerele rezolvate! Mergi la steag \\ud83c\\udfc1 ca să evadezi.'
: 'Mergi la ușa următoare (săgeți / WASD / butoane).';
}
function owCenter(){
var vpW = owWrap.clientWidth, vpH = owWrap.clientHeight;
var worldW = OW_COLS * OW_TILE, worldH = OW_ROWS * OW_TILE;
var px = owPlayer.col * OW_TILE + OW_TILE / 2, py = owPlayer.row * OW_TILE + OW_TILE / 2;
var tx = worldW <= vpW ? (vpW - worldW) / 2 : Math.max(vpW - worldW, Math.min(0, vpW / 2 - px));
var ty = worldH <= vpH ? (vpH - worldH) / 2 : Math.max(vpH - worldH, Math.min(0, vpH / 2 - py));
owWorld.style.transform = 'translate(' + tx + 'px,' + ty + 'px)';
}
function owRenderPlayer(){ if (owPlayerEl){ owPlayerEl.style.left = (owPlayer.col * OW_TILE) + 'px'; owPlayerEl.style.top = (owPlayer.row * OW_TILE) + 'px'; } owCenter(); }
function owWalkable(col, row){ if (col < 0 || row < 0 || col >= OW_COLS || row >= OW_ROWS) return false; return owMap[row][col] !== 1; }
function owMove(dc, dr){
if (!owActive) return;
var nc = owPlayer.col + dc, nr = owPlayer.row + dr;
if (!owWalkable(nc, nr)) return;
owPlayer.col = nc; owPlayer.row = nr; owRenderPlayer(); owCheckEnter();
}
function owCheckEnter(){
for (var i = 0; i < owDoors.length; i++){ var d = owDoors[i]; if (owPlayer.col === d.col && owPlayer.row === d.row){ if (!roomDone[d.idx]) owEnterDoor(d.idx); return; } }
if (owPlayer.col === owExit.col && owPlayer.row === owExit.row && owAllDone()){ owActive = false; showFinale(); }
}
function owEnterDoor(idx){ if (!owActive) return; /* idempotență — a doua intrare ignorată (T4/D4) */ owActive = false; mountRoom(idx); }
function showOverworld(targetIdx, data){
hideAll();
owTargetIdx = targetIdx;
owRefreshDoors();
owRenderPlayer();
owActive = true;
overworldEl.classList.add('show');
if (data){
var s = data.stars || 0;
var letter = String(data.letter || '').replace(/[^A-Za-z0-9]/g, '').charAt(0).toUpperCase();
var t = (letter ? ('+' + letter + ' ') : '') + (s ? ('+' + s + ' \\u2605') : '');
var toast = document.getElementById('ow-toast');
if (t.trim()){ toast.textContent = t; toast.classList.add('show'); setTimeout(function(){ toast.classList.remove('show'); }, 1600); }
}
setTimeout(owCenter, 0);
}
document.addEventListener('keydown', function(e){
if (!owActive) return;
var m = { ArrowUp:[0,-1], ArrowDown:[0,1], ArrowLeft:[-1,0], ArrowRight:[1,0], w:[0,-1], s:[0,1], a:[-1,0], d:[1,0] }[e.key];
if (!m) return; e.preventDefault(); owMove(m[0], m[1]);
});
document.querySelectorAll('#ow-dpad button[data-d]').forEach(function(b){
b.addEventListener('click', function(){ var m = { U:[0,-1], D:[0,1], L:[-1,0], R:[1,0] }[b.getAttribute('data-d')]; if (m) owMove(m[0], m[1]); });
});
/* Hooks pentru teste (conduc harta fără tastatură) */
window.__ow = {
get state(){ return { player: { col: owPlayer.col, row: owPlayer.row }, target: owTargetIdx, active: owActive, allDone: owAllDone(), doors: owDoors.map(function(d){ return { idx: d.idx, col: d.col, row: d.row, solved: !!roomDone[d.idx] }; }) }; },
enterDoor: function(i){ var d = owDoors[i]; if (d){ owPlayer.col = d.col; owPlayer.row = d.row; owRenderPlayer(); owCheckEnter(); } },
enterExit: function(){ owPlayer.col = owExit.col; owPlayer.row = owExit.row; owRenderPlayer(); owCheckEnter(); }
};
owBuild();
/* ----- Intro ----- */
document.getElementById('intro-title').textContent = MASTER.title;
var _introStory = (MASTER.player?'Salut, '+MASTER.player+'! ':'')+MASTER.story;
document.getElementById('intro-story').textContent = _introStory;
document.getElementById('intro-promise').textContent = N+' camere \\u00b7 ${nStyles} stiluri \\u00b7 1 cuvânt magic';
document.getElementById('btn-start').onclick = function(){
unlockAudio(); /* gest direct pe părinte (handlerul global prinde și el, dar e idempotent) */
clearProgress(); owResetPlayer(); showOverworld(0);
voiceSay(_introStory); /* după showOverworld → hideAll a rulat deja, nu mai anulează (D10) */
};
/* ----- Buton voce în bara chrome (D10) ----- */
var btnVoice = document.getElementById('btn-voice');
if(SPEECH && btnVoice){
btnVoice.hidden = false;
var _syncVoiceBtn = function(){
btnVoice.innerHTML = voiceOn ? '&#128266;' : '&#128263;'; /* 🔊 / 🔇 */
btnVoice.setAttribute('aria-pressed', voiceOn ? 'true' : 'false');
btnVoice.title = voiceOn ? 'Naratiune pornita — apasa ca sa opresti' : 'Naratiune oprita — apasa ca sa pornesti';
};
_syncVoiceBtn();
btnVoice.onclick = function(){
voiceOn = !voiceOn;
if(!voiceOn) voiceCancel();
_syncVoiceBtn();
};
}
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 pe hartă, la ușa camerei next */
var resumeIdx = saved.idx + 1;
/* marchează ușile deja rezolvate pe hartă (resume) */
for(var di=0; di<=saved.idx; di++){ roomDone[di] = true; setDot(di,'done'); }
if(resumeIdx >= N){
/* ultima cameră deja terminată — mergi direct la final */
showFinale(); return;
}
owResetPlayer(); showOverworld(resumeIdx);
})();
<\/script>
</body>
</html>`;
}
/* ---------- start ---------- */
renderGlobals();
renderPuzzles();
refreshPreview();
</script>
</body>
</html>