Files
escape-builder/escape-builder.html
2026-06-14 16:36:08 +00:00

3214 lines
165 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; }
/* Share section */
.share-url { width: 100%; font: 12px/1.4 ui-monospace,monospace; padding: 6px 8px; border: 1px solid var(--line); border-radius: 7px; background: #f9fafb; color: var(--ink); resize: none; }
.share-btns { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.share-btns button { flex: 1; min-width: 110px; font-size: 12px; padding: 6px 10px; }
#qrBox svg { display: block; margin: 10px auto 0; max-width: 180px; height: auto; border: 1px solid var(--line); border-radius: 6px; }
/* QR Card print */
#qrCard {
display: none; position: fixed; inset: 0; z-index: 9999;
background: #fff; color: #111;
font-family: system-ui, sans-serif;
flex-direction: column; align-items: center; justify-content: center;
padding: 30mm 20mm; gap: 12px; text-align: center;
}
#qrCard.show { display: flex; }
#qrCard .qr-title { font-size: 22px; font-weight: 900; margin: 0; }
#qrCard .qr-instr { font-size: 13px; color: #555; max-width: 70mm; line-height: 1.5; }
#qrCard .qr-url { font-family: ui-monospace,monospace; font-size: 9px; color: #555; word-break: break-all; max-width: 100mm; margin-top: 4px; }
#qrCard .qr-for { font-size: 14px; font-weight: 700; }
#qrCard svg { width: 60mm; height: 60mm; }
#qrCard .qr-back { margin-top: 16px; font-size: 12px; }
@media print {
body * { visibility: hidden; }
#qrCard, #qrCard * { visibility: visible; }
#qrCard { position: fixed; inset: 0; display: flex !important; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
#qrCard .qr-back { display: none; }
}
</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>Creat de (optional, apare pe diploma)</label>
<input type="text" id="gCreator" data-g="creator" placeholder="ex: Doamna invatatoare">
<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 class="ck"><input type="checkbox" id="gMusic" data-gb="music"> Muzica de fundal &mdash; arpegiu calm care accelereaza sub 1 minut (doar in Campanie; buton de oprire in bara)</label>
<label class="ck"><input type="checkbox" id="gAdventure" data-gb="adventure"> Mod aventura &mdash; raspunsul decide urmatoarea camera (ramificare); configureaza per-puzzle mai jos (doar in Campanie)</label>
<label>Timp limita (minute, 0 = fara) &mdash; ceas calm in bara, doar in Campanie</label>
<input type="number" id="gTimer" data-g="timerMin" min="0" max="120" step="1" placeholder="0">
<div class="help">Pornesti la "Incepe aventura". Sub 1 minut devine auriu. La expirare ingheata pe 0:00 &mdash; jocul continua, fara penalizare.</div>
<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>
<fieldset id="fsShare">
<legend>Distribuie (link + QR)</legend>
<label>URL baza player (GitHub Pages)</label>
<input type="text" id="gBaseUrl" data-g="baseUrl" placeholder="https://USERNAME.github.io/escape-builder/play.html">
<div class="help" style="margin-bottom:8px">Inlocuieste USERNAME cu contul tau GitHub. <a href="https://pages.github.com/" target="_blank" rel="noopener">Cum activezi GitHub Pages &rarr;</a></div>
<div class="share-btns">
<button id="btnShare">Genereaza QR / link</button>
<button id="btnCopyLink">Copiaza link</button>
<button id="btnDownloadPlayer">Descarca player.html</button>
<button id="btnPrintQr">Printeaza cardul QR</button>
</div>
<div id="qrBox"></div>
<textarea id="shareUrl" class="share-url" rows="2" readonly placeholder="Link-ul va aparea aici dupa generare..."></textarea>
</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>
<!-- Card QR print (vizibil doar la print sau la preview) -->
<div id="qrCard" role="document" aria-label="Card QR de distribuit">
<p class="qr-title" id="qrCardTitle"></p>
<div id="qrCardSvg"></div>
<p class="qr-instr">Scaneaza codul QR cu telefonul<br>pentru a juca escape room-ul!</p>
<p class="qr-url" id="qrCardUrl"></p>
<p class="qr-for" id="qrCardFor"></p>
<button class="qr-back" id="btnQrBack">Inchide previzualizarea</button>
</div>
<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' };
/* Stiluri top-level valide (gameHTML rutează pe ele); orice altceva → fallback classic (T5, D8) */
const TOP_STYLES = ['classic', 'terminal', 'arcade', 'chat', 'point', 'campaign'];
const defaultState = () => ({
title: 'Comoara ascunsa',
player: '',
color: '#6d28d9',
style: 'classic',
creator: '',
charName: 'Alex',
voice: false,
music: false,
adventure: false,
timerMin: 0,
baseUrl: 'https://romfast.github.io/escape-builder/play.html',
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 = '';
if (typeof p.branch !== 'object' || Array.isArray(p.branch) || p.branch === null) p.branch = {};
return p;
}
const blankPuzzle = () => normalizePuzzle({ title: '', type: 'free', question: '', answer: '', tfAnswer: 'Adevarat', choices: '', hint: '', letter: '', style: '' });
let state = Object.assign(defaultState(), load() || {});
if (!TOP_STYLES.includes(state.style)) state.style = 'classic'; /* storage corupt → fallback */
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 choiceOptsBuilder(p) {
return (p.choices || '').split('\n').map(l => l.trim()).filter(Boolean)
.map(o => o.charAt(0) === '*' ? o.slice(1).trim() : o);
}
function branchTargetSelect(p, pi, key, n) {
const branch = p.branch || {};
const cur = branch[key] !== undefined ? String(branch[key]) : '';
let opts = `<option value="" ${cur === '' ? 'selected' : ''}>Liniar (urmatoarea)</option>`;
for (let k = 0; k < n; k++) {
if (k === pi) continue;
opts += `<option value="${k}" ${cur === String(k) ? 'selected' : ''}>Camera ${k + 1}</option>`;
}
opts += `<option value="end" ${cur === 'end' ? 'selected' : ''}>Sfarsit</option>`;
return `<select data-fb data-bkey="${esc(key)}">${opts}</select>`;
}
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>
${state.style === 'campaign' && state.adventure ? `
<div style="margin-top:10px;padding:10px 0 0;border-top:1px solid rgba(255,255,255,.1)">
<div style="font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:#a78bfa;font-weight:700;margin-bottom:6px">Ramificare (Adventure Mode)</div>
${p.type === 'free' ? `
<div class="row"><div><label>Daca raspunde corect &rarr; mergi la</label>${branchTargetSelect(p, i, '*', state.puzzles.length)}</div></div>` : ''}
${p.type === 'tf' ? `
<div class="row"><div><label>Daca <strong>Adevarat</strong> &rarr; mergi la</label>${branchTargetSelect(p, i, 'Adevarat', state.puzzles.length)}</div></div>
<div class="row"><div><label>Daca <strong>Fals</strong> &rarr; mergi la</label>${branchTargetSelect(p, i, 'Fals', state.puzzles.length)}</div></div>` : ''}
${p.type === 'choice' ? choiceOptsBuilder(p).map((opt, ci) => `
<div class="row"><div><label>Daca <strong>${esc(opt)}</strong> &rarr; mergi la</label>${branchTargetSelect(p, i, String(ci), state.puzzles.length)}</div></div>`).join('') : ''}
<div class="help" style="color:#fbbf24">&#9888; Reordonarea sau stergerea puzzle-urilor poate invalida ramificarile &mdash; verifica-le dupa!</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;
if (el.dataset.gb === 'adventure') renderPuzzles(); /* re-render: branch dropdowns apar/dispar */
onChange();
});
});
puzzleList.addEventListener('input', e => {
/* branch key selects (adventure mode) */
if (e.target.hasAttribute('data-fb')) {
const bkey = e.target.dataset.bkey;
const i = +e.target.closest('.puzzle').dataset.i;
if (!state.puzzles[i].branch) state.puzzles[i].branch = {};
state.puzzles[i].branch[bkey] = e.target.value;
onChange();
return;
}
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(); }
if (f === 'choices') renderPuzzles(); /* re-render: branch dropdowns per opțiune */
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);
let styleWarn = '';
if (!TOP_STYLES.includes(state.style)) { styleWarn = ' Stil necunoscut „' + state.style + '" — am rotit la „Clasic".'; state.style = 'classic'; }
state.puzzles = state.puzzles.map(normalizePuzzle);
renderGlobals(); renderPuzzles(); onChange();
if (styleWarn) alert('Proiect incarcat.' + styleWarn);
} 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));
delete s.baseUrl; /* metadata de share — nu intră în payload/QR/JSON exportat */
s.timerMin = Math.max(0, Math.min(120, parseInt(s.timerMin, 10) || 0)); /* T10: minute întregi 0..120 */
const nP = s.puzzles.length;
s.puzzles.forEach((p, pi) => {
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);
if (!s.adventure) {
delete p.branch; /* strip branch când adventure e off */
} else {
/* clamp ținte out-of-range → '' (liniar) */
const br = p.branch || {};
Object.keys(br).forEach(k => {
const v = br[k];
if (v !== '' && v !== 'end') {
const n = +v;
if (isNaN(n) || n < 0 || n >= nP) br[k] = '';
}
});
p.branch = br;
}
});
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);
}
/* compresie URL (Iteratia 3) */
const CS_OK = typeof CompressionStream !== 'undefined' && typeof DecompressionStream !== 'undefined';
function bytesToB64url(bytes) {
const chunks = [];
for (let i = 0; i < bytes.length; i += 8192) {
chunks.push(String.fromCharCode.apply(null, bytes.subarray(i, i + 8192)));
}
return btoa(chunks.join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64urlToBytes(str) {
const b64 = str.replace(/-/g, '+').replace(/_/g, '/');
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
async function deflateToBase64url(str) {
const bytes = new TextEncoder().encode(str);
const cs = new CompressionStream('deflate-raw');
const writer = cs.writable.getWriter();
writer.write(bytes);
writer.close();
const buf = await new Response(cs.readable).arrayBuffer();
return bytesToB64url(new Uint8Array(buf));
}
async function inflateFromBase64url(str) {
const bytes = b64urlToBytes(str);
const ds = new DecompressionStream('deflate-raw');
const writer = ds.writable.getWriter();
writer.write(bytes);
writer.close();
const buf = await new Response(ds.readable).arrayBuffer();
return new TextDecoder().decode(buf);
}
/* QR encoder (Iteratia 3, doar builder) — byte mode, ECC L, versiuni 1-22, 8 masti + penalti */
(function(){
/* GF(256) cu polinom primitiv 0x11d */
const EXP = new Uint8Array(512), LOG = new Uint8Array(256);
(function(){
let x = 1;
for (let i = 0; i < 255; i++) {
EXP[i] = x; LOG[x] = i; x = x < 128 ? x << 1 : (x << 1) ^ 0x11d;
}
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
})();
function gmul(a, b) { return a && b ? EXP[LOG[a] + LOG[b]] : 0; }
/* Reed-Solomon: generator poly de grad n */
function rsGenerator(n) {
let g = [1];
for (let i = 0; i < n; i++) {
const ng = new Array(g.length + 1).fill(0);
for (let j = 0; j < g.length; j++) { ng[j] ^= g[j]; ng[j+1] ^= gmul(g[j], EXP[i]); }
g = ng;
}
return g;
}
function rsRemainder(data, gen) {
const rem = new Array(gen.length - 1).fill(0);
for (const byte of data) {
const factor = byte ^ rem.shift(); rem.push(0);
for (let j = 0; j < rem.length; j++) rem[j] ^= gmul(gen[j+1], factor);
}
return rem;
}
/* Tabele versiune 1-22, ECC L
[dataCodewords, ecCodewordsPerBlock, group1Blocks, group1Words, group2Blocks, group2Words] */
const VER_TABLE = [
null, /* v0 placeholder */
[19,7,1,19,0,0],[34,10,1,34,0,0],[55,15,1,55,0,0],[80,20,2,40,0,0],
[108,26,2,54,0,0],[136,18,2,44,0,0],[156,20,4,32,0,0],[194,24,4,40,0,0],
[232,30,4,36,2,36],[274,18,6,36,2,32],[324,20,6,36,4,32],[370,24,6,36,4,36],
[428,26,8,37,1,37],[461,30,8,40,1,38],[523,22,10,40,2,38],[589,24,12,40,2,37],
[647,28,16,37,0,0],[721,30,12,38,6,37],[795,28,17,35,1,35],[861,28,19,35,2,34],
[932,28,16,38,6,38],[1006,30,17,36,6,36],
];
/* Centru alignment patterns per versiune */
const ALIGN_POS = [
[],[],[6,18],[6,22],[6,26],[6,30],[6,34],
[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],
[6,26,46,66],[6,26,48,70],[6,30,50,70],[6,30,54,74],[6,30,56,74],
[6,34,56,76],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,74,102],
];
/* BCH pentru format info (generator 0x537, 10 bit) */
function bchFormat(data) {
let d = data << 10;
for (let i = 4; i >= 0; i--) if (d & (1 << (i+10))) d ^= (0x537 << i);
return d;
}
/* BCH pentru versiune info (generator 0x1f25, 12 bit, v>=7) */
function bchVersion(ver) {
let d = ver << 12;
for (let i = 5; i >= 0; i--) if (d & (1 << (i+12))) d ^= (0x1f25 << i);
return d;
}
/* Construieste matricea QR */
function buildMatrix(ver) {
const size = ver * 4 + 17;
const mat = Array.from({length: size}, () => new Int8Array(size).fill(-1));
const res = Array.from({length: size}, () => new Uint8Array(size)); /* 1=rezervat */
function setFinder(r, c) {
for (let dr = -1; dr <= 7; dr++) for (let dc = -1; dc <= 7; dc++) {
const rr = r + dr, cc = c + dc;
if (rr < 0 || rr >= size || cc < 0 || cc >= size) continue;
res[rr][cc] = 1;
const inFinder = dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6;
mat[rr][cc] = inFinder ? ((dr===0||dr===6||dc===0||dc===6||
(dr>=2&&dr<=4&&dc>=2&&dc<=4)) ? 1 : 0) : 0;
}
}
setFinder(0,0); setFinder(0,size-7); setFinder(size-7,0);
/* Timing */
for (let i = 8; i < size-8; i++) {
mat[6][i] = mat[i][6] = (i & 1) ? 0 : 1;
res[6][i] = res[i][6] = 1;
}
/* Dark module */
mat[4*ver+9][8] = 1; res[4*ver+9][8] = 1;
/* Alignment patterns */
const apos = ALIGN_POS[ver] || [];
for (const ar of apos) for (const ac of apos) {
if ((ar===6&&ac===6)||(ar===6&&ac===apos[apos.length-1])||(ar===apos[apos.length-1]&&ac===6)) continue;
for (let dr=-2;dr<=2;dr++) for (let dc=-2;dc<=2;dc++) {
res[ar+dr][ac+dc] = 1;
mat[ar+dr][ac+dc] = (Math.abs(dr)===2||Math.abs(dc)===2||(dr===0&&dc===0)) ? 1 : 0;
}
}
/* Rezervare format + versiune */
const fmtCells = [[8,0],[8,1],[8,2],[8,3],[8,4],[8,5],[8,7],[8,8],
[7,8],[5,8],[4,8],[3,8],[2,8],[1,8],[0,8],
[size-1,8],[size-2,8],[size-3,8],[size-4,8],[size-5,8],[size-6,8],[size-7,8],[size-8,8],
[8,size-8],[8,size-7],[8,size-6],[8,size-5],[8,size-4],[8,size-3],[8,size-2],[8,size-1]];
for (const [r,c] of fmtCells) { res[r][c] = 1; mat[r][c] = 0; }
if (ver >= 7) {
for (let i=0;i<6;i++) for (let j=0;j<3;j++) {
res[i][size-11+j]=1; mat[i][size-11+j]=0;
res[size-11+j][i]=1; mat[size-11+j][i]=0;
}
}
return { mat, res, size };
}
function encodeData(text, ver) {
const vtbl = VER_TABLE[ver];
const [, ecPB, g1b, g1w, g2b, g2w] = vtbl;
const totalData = vtbl[0];
const bytes = new TextEncoder().encode(text);
/* Bit buffer */
const bits = [];
const pushBits = (v, n) => { for (let i=n-1;i>=0;i--) bits.push((v>>i)&1); };
pushBits(0b0100, 4); /* mode: byte */
pushBits(bytes.length, 8); /* char count (v1-9: 8 bits) — v10-26: 16 bits */
/* For v10+: char count is 16 bits in byte mode. Versions we use (1-22) span both ranges.
Versions 1-9: 8-bit count; 10-26: 16-bit count. */
/* Recalculate: already pushed 8 bits for v<=9; for v>=10 we need 16-bit. Fix: */
bits.length = 0;
pushBits(0b0100, 4);
if (ver <= 9) pushBits(bytes.length, 8); else pushBits(bytes.length, 16);
for (const b of bytes) pushBits(b, 8);
/* Terminator + pad to byte boundary */
for (let i=0;i<4&&bits.length<totalData*8;i++) bits.push(0);
while (bits.length % 8) bits.push(0);
/* Pad codewords */
const pads = [0xEC, 0x11]; let pi = 0;
while (bits.length < totalData * 8) { pushBits(pads[pi++ % 2], 8); }
/* Convert to bytes */
const data = [];
for (let i=0;i<bits.length;i+=8) data.push(bits.slice(i,i+8).reduce((a,b)=>a*2+b,0));
/* Interleave blocks */
const blocks = [];
let off = 0;
for (let i=0;i<g1b+g2b;i++) {
const w = (i<g1b) ? g1w : g2w;
blocks.push(data.slice(off, off+w)); off += w;
}
const gen = rsGenerator(ecPB);
const ecBlocks = blocks.map(b => rsRemainder(b, gen));
const out = [];
const maxLen = Math.max(...blocks.map(b=>b.length));
for (let i=0;i<maxLen;i++) for (const b of blocks) if (i<b.length) out.push(b[i]);
for (let i=0;i<ecPB;i++) for (const eb of ecBlocks) out.push(eb[i]);
return out;
}
function placeData(mat, res, data, size) {
const bits = [];
for (const b of data) for (let i=7;i>=0;i--) bits.push((b>>i)&1);
let bi = 0, up = true;
for (let col = size-1; col >= 0; col -= 2) {
if (col === 6) col--;
for (let ri = 0; ri < size; ri++) {
const r = up ? size-1-ri : ri;
for (let dc = 0; dc <= 1; dc++) {
const c = col - dc;
if (!res[r][c] && bi < bits.length) { mat[r][c] = bits[bi++]; }
}
}
up = !up;
}
}
const MASK_FNS = [
(r,c)=>(r+c)%2===0,(r,c)=>r%2===0,(r,c)=>c%3===0,(r,c)=>(r+c)%3===0,
(r,c)=>((Math.floor(r/2)+Math.floor(c/3))%2===0),(r,c)=>(r*c)%2+(r*c)%3===0,
(r,c)=>((r*c)%2+(r*c)%3)%2===0,(r,c)=>((r+c)%2+(r*c)%3)%2===0
];
function applyMask(mat, res, size, mi) {
const fn = MASK_FNS[mi];
const m = mat.map(r=>r.slice());
for (let r=0;r<size;r++) for (let c=0;c<size;c++) if (!res[r][c]) m[r][c] ^= fn(r,c)?1:0;
return m;
}
function writeFormat(m, size, maskIdx) {
/* ECC L = 01 */
const raw = (0b01 << 3) | maskIdx;
const fmtBits = ((raw << 10) | bchFormat(raw)) ^ 0x5412;
const seq = [[8,0],[8,1],[8,2],[8,3],[8,4],[8,5],[8,7],[8,8],
[7,8],[5,8],[4,8],[3,8],[2,8],[1,8],[0,8]];
const seqR = [[size-1,8],[size-2,8],[size-3,8],[size-4,8],[size-5,8],[size-6,8],[size-7,8],
[8,size-8],[8,size-7],[8,size-6],[8,size-5],[8,size-4],[8,size-3],[8,size-2],[8,size-1]];
for (let i=0;i<15;i++) {
const bit = (fmtBits >> (14-i)) & 1;
m[seq[i][0]][seq[i][1]] = bit;
m[seqR[i][0]][seqR[i][1]] = bit;
}
}
function writeVersion(m, size, ver) {
if (ver < 7) return;
const raw = ver;
const verBits = (raw << 12) | bchVersion(raw);
for (let i=0;i<18;i++) {
const bit = (verBits >> i) & 1;
const r = Math.floor(i/3), c = size-11+i%3;
m[r][c] = bit; m[c][r] = bit;
}
}
function penaltyScore(m, size) {
let score = 0;
/* Rule 1: 5+ in a row */
for (let r=0;r<size;r++) {
for (let isCol=0;isCol<2;isCol++) {
let run=1, prev=isCol?m[0][r]:m[r][0];
for (let i=1;i<size;i++) {
const v = isCol?m[i][r]:m[r][i];
if (v===prev) { run++; if(run===5) score+=3; else if(run>5) score++; }
else { run=1; prev=v; }
}
}
}
/* Rule 2: 2x2 blocks */
for (let r=0;r<size-1;r++) for (let c=0;c<size-1;c++) {
const v=m[r][c]; if(v===m[r+1][c]&&v===m[r][c+1]&&v===m[r+1][c+1]) score+=3;
}
/* Rule 3: finder-like patterns */
const p1=[1,0,1,1,1,0,1,0,0,0,0], p2=[0,0,0,0,1,0,1,1,1,0,1];
for (let r=0;r<size;r++) for (let c=0;c<=size-11;c++) {
let h1=true,h2=true,v1=true,v2=true;
for (let k=0;k<11;k++) {
if(m[r][c+k]!==p1[k]) h1=false; if(m[r][c+k]!==p2[k]) h2=false;
if(m[c+k][r]!==p1[k]) v1=false; if(m[c+k][r]!==p2[k]) v2=false;
}
if(h1||h2) score+=40; if(v1||v2) score+=40;
}
/* Rule 4: dark module ratio */
let dark=0;
for (let r=0;r<size;r++) for (let c=0;c<size;c++) if(m[r][c]) dark++;
const ratio = Math.abs(dark*100/(size*size)-50);
score += Math.floor(ratio/5)*10;
return score;
}
function makeQrSvg(text, opts={}) {
opts = Object.assign({ quietZone: 4 }, opts);
const bytes = new TextEncoder().encode(text);
const n = bytes.length;
/* Trova minimum version ECC L */
let ver = 0;
for (let v=1; v<=22; v++) {
if (VER_TABLE[v] && VER_TABLE[v][0] >= n + (v<=9?3:4)) { ver=v; break; }
}
if (!ver) return null; /* text too long */
const data = encodeData(text, ver);
const { mat, res, size } = buildMatrix(ver);
placeData(mat, res, data, size);
/* Alege masca cu penalti minim */
let bestMask = 0, bestScore = Infinity;
for (let mi=0;mi<8;mi++) {
const m2 = applyMask(mat, res, size, mi);
writeFormat(m2, size, mi);
writeVersion(m2, size, ver);
const sc = penaltyScore(m2, size);
if (sc < bestScore) { bestScore=sc; bestMask=mi; }
}
const finalMat = applyMask(mat, res, size, bestMask);
writeFormat(finalMat, size, bestMask);
writeVersion(finalMat, size, ver);
/* Genereaza SVG cu un singur <path> (run-uri per rand) */
const qz = opts.quietZone;
const total = size + 2*qz;
let d = '';
for (let r=0;r<size;r++) {
let run=0, startC=-1;
for (let c=0;c<=size;c++) {
const on = c<size && finalMat[r][c]===1;
if (on && run===0) { startC=c; run=1; }
else if (on) run++;
else if (run>0) {
d += `M${startC+qz},${r+qz}h${run}v1h-${run}z`;
run=0;
}
}
}
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${total} ${total}" shape-rendering="crispEdges"><rect width="${total}" height="${total}" fill="#fff"/><path fill="#000" d="${d}"/></svg>`;
}
window.makeQrSvg = makeQrSvg;
})();
/* ---------- 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 + helperii partajați (norm/beep/confetti/checkAnswer/starsFor/finalWord/
choiceOpts/campaignDone/roomReady/onerror) vin din libJS(cfg) injectat în <script> (D7) */
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>
${libJS(cfg)}
document.documentElement.style.setProperty('--accent-light', 'color-mix(in srgb, ' + (CFG.color || '#6d28d9') + ' 40%, white)');
var idx = 0, attempts = 0, hintUsed = false, won = [];
/* CFG, totalStars, el, norm, beep, confetti, starsFor, finalWord, checkAnswer,
choiceOpts, campaignDone, roomReady, window.onerror — toate din libJS (D7) */
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(p, inp.value); };
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(p, v); };
box.appendChild(b);
});
} else {
var opts = choiceOpts(p);
opts.forEach(function (o) {
var b = document.createElement('button');
b.className = 'opt'; b.textContent = o;
b.onclick = function () { check(p, o); };
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(p, given) {
if (checkAnswer(p, given)) {
var stars = starsFor(attempts, hintUsed);
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){ campaignDone(); return; }
show('sFinal');
var max = CFG.puzzles.length * 3;
el('finalStars').textContent = totalStars + ' / ' + max + ' \\u2605';
var word = finalWord();
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();
}
roomReady(); /* beep/confetti/onerror/roomReady din libJS (D7) */
<\/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 ''; }
var _lastGiven = '';
function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type === 'choice' ? choiceCorrect(p) : p.answer); var ok = norm(given) !== '' && norm(given) === norm(exp); if(ok){ _lastGiven = given; } return ok; }
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{ var p = CFG.puzzles[0]; var bkey = '*'; if(p.type === 'tf'){ bkey = _lastGiven || 'Adevarat'; } else if(p.type === 'choice'){ var opts = choiceOpts(p); var bi = opts.findIndex(function(o){ return norm(o) === norm(_lastGiven); }); bkey = String(bi >= 0 ? bi : 0); } parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:finalWord().charAt(0), branch:bkey}); }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(); };`;
/* HUD partajat (arcade + point): scor + bara de litere câștigate. isSolved(j)→bool
diferă per motor (doorsSolved vs solvedFlags) → injectat ca funcție (T8). */
SNIP.hudJs = `function hudLetters(isSolved){
el('hudStars').textContent = totalStars + ' \\u2605';
var hb = el('hudLetters'); hb.innerHTML = '';
for (var j = 0; j < CFG.puzzles.length; j++){
var L = (CFG.puzzles[j].letter || '').trim(); if (!L) continue;
var s = document.createElement('span');
if (isSolved(j)){ s.textContent = L.toUpperCase(); s.className = 'won'; } else s.textContent = '?';
hb.appendChild(s);
}
}`;
/* helperi inflate pt. player (player-side, backslash dublu pentru regex) */
SNIP.compressJs = `function bytesToB64url(bytes){
var chunks=[];
for(var i=0;i<bytes.length;i+=8192)chunks.push(String.fromCharCode.apply(null,bytes.subarray(i,i+8192)));
return btoa(chunks.join('')).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=/g,'');
}
function b64urlToBytes(str){
var b64=str.replace(/-/g,'+').replace(/_/g,'/');
var bin=atob(b64);var bytes=new Uint8Array(bin.length);
for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);
return bytes;
}
async function inflateFromBase64url(str){
var bytes=b64urlToBytes(str);
var ds=new DecompressionStream('deflate-raw');
var writer=ds.writable.getWriter();writer.write(bytes);writer.close();
var buf=await new Response(ds.readable).arrayBuffer();
return new TextDecoder().decode(buf);
}`;
/* ---------- 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);
hudLetters(function(j){ return puzzleProgress && puzzleProgress.doorsSolved[j]; });
}
/* ----- 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.hudJs}
${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;
hudLetters(function(j){ return solvedFlags[j]; });
}
${SNIP.hudJs}
${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 campaignShell({ tplJson, masterExpr, titleExpr, nStyles, bootMode }) {
const _scriptOpen = bootMode === 'hash'
? `<script>
${SNIP.compressJs}
var TPL = ${tplJson};
(async function(){
var h=location.hash.slice(1);
if(!h){document.getElementById('intro-title').textContent='Niciun joc în acest link.';return;}
window.MASTER=JSON.parse(await inflateFromBase64url(h));
var s=document.createElement('script');s.textContent=document.getElementById('run').textContent;document.body.appendChild(s);
})();
<\/script>
<script type="text/plain" id="run">
var MASTER=window.MASTER;`
: `<script>
var TPL = ${tplJson};
var MASTER = ${masterExpr};`;
return `<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${titleExpr}</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; }
/* Timer Calm (§Design pct.10) — neutru; auriu sub 1 min; înghețat la expirare (fără roșu pulsant) */
#chrome-timer {
font-variant-numeric: tabular-nums; font-weight: 700; font-size: 15px;
color: var(--c-ink); letter-spacing: .02em; min-width: 3.1em; text-align: right;
}
#chrome-timer[hidden] { display: none; }
#chrome-timer.low { color: var(--c-gold); }
#chrome-timer.expired { color: var(--c-gold); opacity: .55; }
#btn-voice, #btn-music {
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], #btn-music[hidden] { display: none; } /* id batea specificitatea UA [hidden] */
#btn-voice:hover, #btn-music:hover { background: rgba(255,255,255,.22); }
#btn-voice[aria-pressed="false"], #btn-music[aria-pressed="false"] { opacity: .5; }
#btn-voice:focus-visible, #btn-music: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-door.locked { background: #374151; filter: grayscale(1) brightness(.65); opacity: .7; cursor: not-allowed; }
.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-sec {
font: inherit; font-size: 15px; font-weight: 700;
background: rgba(255,255,255,.12); color: #fff; border: 1px solid rgba(255,255,255,.22);
border-radius: 12px; padding: 12px 24px; cursor: pointer; min-height: 44px;
width: 100%; max-width: 320px;
}
.btn-sec:hover { background: rgba(255,255,255,.2); }
.btn-main:focus-visible, .btn-sec:focus-visible { outline: 2px solid #a78bfa; outline-offset: 2px; }
.fin-actions, .dipl-actions { display: flex; flex-direction: column; align-items: center; gap: 10px; width: 100%; }
/* ----- Diplomă A4 print-first (§Design pct.9) ----- */
#diploma { background: #0d0620; gap: 16px; }
.dipl-sheet {
width: 100%; max-width: 520px; aspect-ratio: 210 / 297; background: #fff; color: #1a1333;
border-radius: 6px; box-shadow: 0 18px 50px rgba(0,0,0,.5);
display: flex; padding: 10px; overflow: hidden;
}
.dipl-frame {
flex: 1; border: 3px double var(--accent); border-radius: 4px;
display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
gap: 2.2%; padding: 6% 7%; text-align: center;
}
.dipl-title { font-family: Georgia, "Times New Roman", serif; font-weight: 700; letter-spacing: .04em;
font-size: clamp(20px, 5.2vw, 30px); color: var(--accent); }
.dipl-sub { font-size: clamp(11px, 2.4vw, 13px); color: #6b6480; text-transform: uppercase; letter-spacing: .12em; }
.dipl-name { font-size: clamp(26px, 7vw, 42px); font-weight: 800; line-height: 1.05; color: #1a1333; word-break: break-word; }
.dipl-game { font-size: clamp(12px, 2.8vw, 15px); color: #4a4360; font-style: italic; }
.dipl-rooms { display: flex; flex-direction: column; gap: 3px; width: 100%; max-width: 320px; margin-top: 2%; }
.dipl-rooms .dipl-room { display: flex; justify-content: space-between; align-items: center; gap: 8px;
font-size: clamp(11px, 2.4vw, 13px); color: #4a4360; border-bottom: 1px dotted rgba(0,0,0,.12); padding: 2px 0; }
.dipl-rooms .dipl-room .rstars { color: #c8952a; letter-spacing: 1px; white-space: nowrap; }
.dipl-rooms .dipl-room .rskip { color: #9a93ad; }
.dipl-wordlbl { font-size: clamp(10px, 2.2vw, 12px); text-transform: uppercase; letter-spacing: .12em; color: #6b6480; margin-top: 2%; }
.dipl-word { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }
.dipl-word span { width: clamp(24px, 7vw, 38px); aspect-ratio: 5 / 6; background: var(--accent); color: #fff;
border-radius: 7px; display: flex; align-items: center; justify-content: center; font-size: clamp(15px, 4vw, 22px); font-weight: 800; }
.dipl-word span.lock { background: #d8d3e4; color: #6b6480; }
.dipl-footer { margin-top: auto; font-size: clamp(10px, 2.2vw, 12px); color: #6b6480; line-height: 1.5; }
.dipl-footer .dipl-expired { color: #c8952a; }
@media print {
body * { visibility: hidden !important; }
#diploma, #diploma * { visibility: visible !important; }
#diploma { position: fixed; inset: 0; display: flex !important; background: #fff !important; padding: 0; }
.dipl-actions { display: none !important; }
.dipl-sheet { box-shadow: none; max-width: none; width: auto; height: auto; margin: 20mm; aspect-ratio: 210 / 297; }
.dipl-title, .dipl-word span, .dipl-frame { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
.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; }
#chrome-timer { font-size: 13px; }
#dots span { width: 8px; height: 8px; }
}
</style>
</head>
<body>
<div id="chrome">
<span id="chrome-title">${titleExpr}</span>
<div class="sp"></div>
<span id="chrome-timer" role="timer" aria-label="Timp ramas" hidden>0:00</span>
<button id="btn-music" type="button" aria-label="Muzica de fundal" hidden>&#127925;</button>
<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 class="fin-actions">
<button class="btn-main" id="btn-diploma">Vezi diploma &rarr;</button>
<button class="btn-sec" id="btn-replay">Joacă din nou</button>
</div>
</div>
<!-- Diplomă A4 print-first (§Design pct.9) — populată la „Vezi diploma" -->
<div id="diploma" class="overlay" aria-hidden="true">
<div class="dipl-sheet" role="document" aria-label="Diplomă de evadare">
<div class="dipl-frame">
<div class="dipl-title">DIPLOMĂ DE EVADARE</div>
<div class="dipl-sub">se acordă lui</div>
<div class="dipl-name" id="dipl-name"></div>
<div class="dipl-game" id="dipl-game"></div>
<div class="dipl-rooms" id="dipl-rooms"></div>
<div class="dipl-wordlbl">Cuvântul magic</div>
<div class="dipl-word" id="dipl-word"></div>
<div class="dipl-footer" id="dipl-footer"></div>
</div>
</div>
<div class="dipl-actions">
<button class="btn-main" id="dipl-print">Printează diploma</button>
<button class="btn-sec" id="dipl-back">&larr; Înapoi</button>
</div>
</div>
</div>
${_scriptOpen}
/*
* 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 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 roomStars = []; /* stele per cameră — pentru diplomă (§Design pct.9) */
var skipped = {};
var activeIdx = -1;
var activeWindow = null;
var readyTimer = null;
var roomDone = {};
var ADVENTURE = !!MASTER.adventure;
var owUnlocked = {0: true}; /* ușile deblocate în adventure mode */
var owExitUnlocked = false;
/* ----- 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(){
var payload = { idx: activeIdx, totalStars: totalStars, collected: collected.slice(), roomStars: roomStars.slice(), skipped: skipped };
if(ADVENTURE){
payload.doneList = Object.keys(roomDone).map(Number);
payload.owUnlocked = owUnlocked;
payload.owExitUnlocked = owExitUnlocked;
payload.target = owTargetIdx;
}
safeSet(payload);
}
function clearProgress(){ try{ sessionStorage.removeItem(_RESUME_KEY); sessionStorage.removeItem(_DEADLINE_KEY); }catch(e){} }
/* ----- Timer Calm (§Design pct.10) — ceas M:SS în chrome.
Pornește la „Începe aventura"; deadline ABSOLUT în sessionStorage → resume-ul
(reload mid-campanie) NU resetează ceasul. Sub 1 min → auriu. La expirare îngheață
pe 0:00 + marcaj discret, jocul curge nestingherit (zero penalizare). ----- */
var TIMER_SEC = (+MASTER.timerMin || 0) * 60;
var _DEADLINE_KEY = _RESUME_KEY + '-dl';
var timerEl = document.getElementById('chrome-timer');
var _deadline = 0, _timerInt = null, _timerExpired = false;
function _fmt(s){ var m = Math.floor(s/60), ss = s % 60; return m + ':' + (ss < 10 ? '0' : '') + ss; }
function tickTimer(){
if(!_deadline){ return; }
var rem = Math.round((_deadline - Date.now()) / 1000);
if(rem <= 0){
rem = 0;
if(!_timerExpired){ _timerExpired = true; timerEl.classList.add('expired'); timerEl.title = 'Timpul a expirat — jocul continua'; }
if(_timerInt){ clearInterval(_timerInt); _timerInt = null; }
}
timerEl.textContent = _fmt(rem);
if(rem <= 60) timerEl.classList.add('low');
}
function stopTimer(){ if(_timerInt){ clearInterval(_timerInt); _timerInt = null; } }
function startTimer(){
if(TIMER_SEC <= 0) return;
timerEl.hidden = false;
var existing = 0; try{ existing = +sessionStorage.getItem(_DEADLINE_KEY) || 0; }catch(e){}
if(existing > 0){ _deadline = existing; } /* resume → păstrează ceasul */
else { _deadline = Date.now() + TIMER_SEC * 1000; try{ sessionStorage.setItem(_DEADLINE_KEY, String(_deadline)); }catch(e){} }
tickTimer();
if(!_timerInt && !_timerExpired) _timerInt = setInterval(tickTimer, 1000);
}
/* ----- Muzică ambient (T10) — opt-in MASTER.music. Orchestrator-only: părintele
deține AudioContext (reutilizează beep._ctx, deblocat de gestul global); camerele
nu știu de muzică. Arpegiu calm pe pentatonică minoră; tempo ACCELEREAZĂ sub 1 min
(legat de Timer Calm). Se atenuează (duck) cât timp vocea vorbește. Fallback:
fără AudioContext → no-op, butonul rămâne ascuns (zero penalizare). ----- */
var MUSIC = !!MASTER.music;
var musicOn = MUSIC;
var _mGain = null, _mTimer = null, _mStep = 0, _mDuck = 1;
var _MSCALE = [0, 3, 5, 7, 10]; /* pentatonică minoră (semitonuri) */
function _mFreq(semi){ return 220 * Math.pow(2, semi / 12); }
function musicTempoFactor(){
/* 1.0 normal → ~1.8 pe ultimul minut (accelerare progresivă) */
if(TIMER_SEC <= 0 || !_deadline) return 1;
var rem = (_deadline - Date.now()) / 1000;
if(rem < 0) return 1.8;
if(rem > 60) return 1;
return 1 + (1 - rem / 60) * 0.8;
}
function _mTick(){
if(!musicOn || !_mGain){ _mTimer = null; return; }
var ctx = beep._ctx;
if(ctx){
try{
var oct = (Math.floor(_mStep / _MSCALE.length) % 2) ? 12 : 0;
var semi = _MSCALE[_mStep % _MSCALE.length] + oct;
var o = ctx.createOscillator(), g = ctx.createGain(), t = ctx.currentTime;
o.type = 'sine'; o.frequency.value = _mFreq(semi);
g.gain.setValueAtTime(0.0001, t);
g.gain.linearRampToValueAtTime(0.05 * _mDuck, t + 0.05);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.55);
o.connect(g); g.connect(_mGain);
o.start(t); o.stop(t + 0.6);
}catch(e){}
}
_mStep++;
_mTimer = setTimeout(_mTick, 520 / musicTempoFactor()); /* mai rapid când tempo crește */
}
function startMusic(){
if(!musicOn) return;
try{
var ctx = beep._ctx || (beep._ctx = new (window.AudioContext || window.webkitAudioContext)());
if(ctx.state === 'suspended') ctx.resume();
if(!_mGain){ _mGain = ctx.createGain(); _mGain.gain.value = 1; _mGain.connect(ctx.destination); }
if(!_mTimer){ _mStep = 0; _mTick(); }
}catch(e){}
}
function stopMusic(){ if(_mTimer){ clearTimeout(_mTimer); _mTimer = null; } }
function duckMusic(on){ _mDuck = on ? 0.22 : 1; } /* vocea are prioritate (edge T10) */
window.__music = { tempo: musicTempoFactor, state: function(){ return { on: musicOn, playing: !!_mTimer, duck: _mDuck }; } };
var frameEl = document.getElementById('room-frame');
var introEl = document.getElementById('intro');
var skipEl = document.getElementById('skip-banner');
var finaleEl = document.getElementById('finale');
var diplomaEl = document.getElementById('diploma');
/* ----- 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){} } duckMusic(false); }
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;
/* vocea are prioritate → atenuează muzica cât timp vorbește (edge T10) */
u.onstart = function(){ duckMusic(true); };
u.onend = function(){ duckMusic(false); };
u.onerror = function(){ duckMusic(false); };
window.speechSynthesis.speak(u);
}catch(e){}
}
window.voiceSay = voiceSay; /* expus camerelor: parent.voiceSay(replica) — guard în motoare */
/* ----- parent.* API ----- */
/* Calculează camera țintă după răspuns (adventure mode).
Returnează număr (idx cameră) sau 'end'. Non-adventure → idx+1. */
function resolveBranch(idx, key){
if(!ADVENTURE) return idx + 1;
var p = MASTER.puzzles[idx];
var br = p && p.branch;
var t = br ? (br[key] !== undefined ? br[key] : br['*']) : undefined;
if(t === undefined || t === '') t = idx + 1; /* fallback liniar */
if(t === 'end') return 'end';
t = +t;
if(isNaN(t) || t < 0 || t >= N) return 'end';
return t;
}
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);
roomStars[idx] = (data.stars || 0); /* pentru diplomă */
var letter = String(data.letter || '').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
if(letter) collected.push(letter);
setDot(idx,'done');
console.log('[campaign] camera',idx,'done. stars=',data.stars,'letter=',letter);
if(ADVENTURE){
var dest = resolveBranch(idx, data.branch || '*');
if(dest === 'end'){
owExitUnlocked = true;
saveProgress();
showOverworld(idx, data); /* overworld cu exit deblocat — player merge la steag */
} else {
owUnlocked[dest] = true;
owTargetIdx = dest;
saveProgress();
showOverworld(dest, data);
}
} else {
saveProgress();
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(){
stopTimer(); stopMusic(); /* jocul s-a încheiat — oprește ceasul + muzica */
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) */
}
/* ----- Diplomă A4 (§Design pct.9) — populată la „Vezi diploma" ----- */
function _starStr(n){ n = Math.max(0, Math.min(3, n|0)); var s = ''; for(var i=0;i<3;i++) s += i<n ? '\\u2605' : '\\u2606'; return s; }
function buildDiploma(){
document.getElementById('dipl-name').textContent = (MASTER.player||'').trim() || 'Campion';
document.getElementById('dipl-game').textContent = '\\u201E' + (MASTER.title||'') + '\\u201D';
var rooms = document.getElementById('dipl-rooms'); rooms.innerHTML = '';
for(var i=0;i<N;i++){
var row = document.createElement('div'); row.className = 'dipl-room';
var lab = document.createElement('span'); lab.textContent = 'Camera ' + (i+1);
var val = document.createElement('span');
if(skipped[i]){ val.className = 'rskip'; val.textContent = '\\uD83D\\uDD12 s\\u0103rit\\u0103'; }
else if(ADVENTURE && !roomDone[i]){ val.className = 'rskip'; val.textContent = '\\u2014 neexplorat\\u0103'; }
else { val.className = 'rstars'; val.textContent = _starStr(roomStars[i]||0); }
row.appendChild(lab); row.appendChild(val); rooms.appendChild(row);
}
var w = document.getElementById('dipl-word'); w.innerHTML = '';
collected.forEach(function(l){ var s = document.createElement('span'); s.textContent = l; w.appendChild(s); });
Object.keys(skipped).forEach(function(){ var s = document.createElement('span'); s.className = 'lock'; s.textContent = '\\uD83D\\uDD12'; w.appendChild(s); });
var foot = '';
try{ foot = new Date().toLocaleDateString('ro-RO', {year:'numeric', month:'long', day:'numeric'}); }catch(e){ foot = ''; }
var cre = (MASTER.creator||'').trim(); if(cre) foot += ' \\u00b7 creat de ' + cre;
var fEl = document.getElementById('dipl-footer'); fEl.textContent = foot;
if(_timerExpired){ var ex = document.createElement('div'); ex.className = 'dipl-expired'; ex.textContent = 'timpul a expirat'; fEl.appendChild(ex); }
}
function showDiploma(){
buildDiploma();
finaleEl.classList.remove('show');
diplomaEl.classList.add('show'); diplomaEl.setAttribute('aria-hidden','false');
}
function hideDiploma(){
diplomaEl.classList.remove('show'); diplomaEl.setAttribute('aria-hidden','true');
finaleEl.classList.add('show');
}
document.getElementById('btn-diploma').onclick = showDiploma;
document.getElementById('dipl-back').onclick = hideDiploma;
document.getElementById('dipl-print').onclick = function(){ try{ window.print(); }catch(e){} };
document.getElementById('btn-replay').onclick = function(){ clearProgress(); location.reload(); };
/* ----- 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,diplomaEl].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];
var locked = ADVENTURE && !owUnlocked[d.idx] && !done;
el.className = 'ow-door' + (done ? ' solved' : '') + (!done && !locked && d.idx === owTargetIdx ? ' target' : '') + (locked ? ' locked' : '');
if (isSkip) el.textContent = '\\ud83d\\udd12';
else if (done) el.textContent = (MASTER.puzzles[d.idx].letter || '').trim().toUpperCase() || '\\u2713';
else if (locked) el.textContent = '\\ud83d\\udd12';
else el.textContent = (d.idx + 1);
});
var exitOpen = ADVENTURE ? owExitUnlocked : owAllDone();
var ex = document.getElementById('ow-exit'); if (ex) ex.className = 'ow-exit' + (exitOpen ? ' open' : '');
document.getElementById('ow-hint').textContent = exitOpen
? 'Toate camerele rezolvate! Mergi la steag \\ud83c\\udfc1 ca s\\u0103 evadezi.'
: 'Mergi la u\\u015fa urm\\u0103toare (s\\u0103ge\\u021bi / 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] && (!ADVENTURE || owUnlocked[d.idx])) owEnterDoor(d.idx);
return;
}
}
var canExit = ADVENTURE ? owExitUnlocked : owAllDone();
if (owPlayer.col === owExit.col && owPlayer.row === owExit.row && canExit){ owActive = false; clearProgress(); 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(), owUnlocked: owUnlocked, owExitUnlocked: owExitUnlocked, doors: owDoors.map(function(d){ return { idx: d.idx, col: d.col, row: d.row, solved: !!roomDone[d.idx] }; }) }; },
enterDoor: function(i){ var d = owDoors[i]; if (d){ owPlayer.col = d.col; owPlayer.row = d.row; owRenderPlayer(); owCheckEnter(); } },
enterExit: function(){ owPlayer.col = owExit.col; owPlayer.row = owExit.row; owRenderPlayer(); owCheckEnter(); }
};
owBuild();
/* ----- Intro ----- */
document.getElementById('intro-title').textContent = MASTER.title;
document.getElementById('chrome-title').textContent = MASTER.title;
document.title = MASTER.title || document.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();
owUnlocked = {0: true}; owExitUnlocked = false; /* reset adventure state la start nou */
showOverworld(0);
startTimer(); /* ceasul pornește exact la „Începe aventura" (intro necronometrat) */
startMusic(); /* muzica ambient pornește odată cu aventura (T10) */
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();
};
}
/* ----- Buton muzică în bara chrome (T10) ----- */
var btnMusic = document.getElementById('btn-music');
if(MUSIC && btnMusic){
btnMusic.hidden = false;
var _syncMusicBtn = function(){
btnMusic.innerHTML = musicOn ? '&#127925;' : '&#128263;'; /* 🎵 / 🔇 */
btnMusic.setAttribute('aria-pressed', musicOn ? 'true' : 'false');
btnMusic.title = musicOn ? 'Muzica pornita — apasa ca sa opresti' : 'Muzica oprita — apasa ca sa pornesti';
};
_syncMusicBtn();
btnMusic.onclick = function(){
musicOn = !musicOn;
if(musicOn) startMusic(); else stopMusic();
_syncMusicBtn();
};
}
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 || [];
roomStars = saved.roomStars || [];
skipped = saved.skipped || {};
Object.keys(skipped).forEach(function(k){ roomDone[+k] = true; setDot(+k,'done'); });
if(ADVENTURE){
/* Adventure: restore non-contiguous done list (nu bucla liniară 0..idx) */
var dl = saved.doneList || [saved.idx];
dl.forEach(function(i){ if(i >= 0 && i < N){ roomDone[i] = true; setDot(i,'done'); } });
owUnlocked = saved.owUnlocked || {0: true};
owExitUnlocked = !!saved.owExitUnlocked;
var target = (typeof saved.target === 'number' && saved.target >= 0 && saved.target < N) ? saved.target : 0;
owResetPlayer(); showOverworld(target);
startTimer(); startMusic();
return;
}
/* Non-adventure: bucla liniară (comportament existent) */
var resumeIdx = saved.idx + 1;
for(var di=0; di<=saved.idx; di++){ roomDone[di] = true; setDot(di,'done'); }
if(resumeIdx >= N){
showFinale(); return;
}
owResetPlayer(); showOverworld(resumeIdx);
startTimer(); /* resume → reia ceasul de la deadline-ul absolut salvat */
startMusic(); /* resume → reia muzica (T10) */
})();
<\/script>
</body>
</html>`;
}
function gameCampaign(cfg) {
const ROTATION = ['classic', 'terminal', 'arcade', 'chat', 'point'];
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 nStyles = stylesNeeded.size;
return campaignShell({ tplJson, masterExpr: masterJson, titleExpr: esc(cfg.title), nStyles, bootMode: 'inline' });
}
function playerHTML() {
/* Player universal: toate 5 template-uri inline, MASTER vine din hash comprimat */
const ROTATION = ['classic', 'terminal', 'arcade', 'chat', 'point'];
const engines = { classic: gameClassic, terminal: gameTerminal, arcade: gameArcade, chat: gameChat, point: gamePoint };
const templates = {};
for (const style of ROTATION) {
templates[style] = engines[style]('__TEMPLATE__');
}
const tplJson = JSON.stringify(templates).replace(/</g, '\\u003c');
const nStyles = ROTATION.length; /* toate 5 stiluri disponibile */
return campaignShell({ tplJson, masterExpr: '', titleExpr: 'Escape Room Player', nStyles, bootMode: 'hash' });
}
/* ---------- distribuie (link + QR) ---------- */
$('#btnShare').disabled = !CS_OK;
$('#btnCopyLink').disabled = !CS_OK;
if (!CS_OK) {
const t = 'Browserul nu suportă CompressionStream (necesar pentru compresie URL)';
$('#btnShare').title = t; $('#btnCopyLink').title = t;
}
$('#btnShare').addEventListener('click', async () => {
if (state.puzzles.length === 0) { alert('Adaugă cel puțin un puzzle înainte de a genera QR-ul!'); return; }
const payload = await deflateToBase64url(JSON.stringify(cleanState()));
const baseUrl = (state.baseUrl || '').replace(/#.*$/, '');
const url = baseUrl + '#' + payload;
$('#shareUrl').value = url;
const svg = makeQrSvg ? makeQrSvg(url) : null;
if (!svg) {
$('#qrBox').innerHTML = '<p style="color:var(--danger);font-size:12px">URL prea lung pentru QR (încearcă un domeniu mai scurt sau mai puține puzzle-uri).</p>';
} else {
$('#qrBox').innerHTML = svg;
/* Populate QR card for print */
$('#qrCardTitle').textContent = state.title || 'Escape Room';
$('#qrCardFor').textContent = state.player ? 'Pentru: ' + state.player : '';
$('#qrCardUrl').textContent = url;
$('#qrCardSvg').innerHTML = svg;
}
});
$('#btnCopyLink').addEventListener('click', async () => {
const url = $('#shareUrl').value;
if (!url) { alert('Generează mai întâi link-ul cu butonul "Generează QR / link".'); return; }
try {
await navigator.clipboard.writeText(url);
const btn = $('#btnCopyLink');
const old = btn.textContent;
btn.textContent = 'Copiat!';
setTimeout(() => { btn.textContent = old; }, 1500);
} catch (_) {
/* fallback pentru file:// unde clipboard API e blocat */
const ta = document.createElement('textarea');
ta.value = url; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
try { document.execCommand('copy'); } catch (_2) { alert('Copiază manual: ' + url); }
document.body.removeChild(ta);
}
});
$('#btnDownloadPlayer').addEventListener('click', () => {
download('player.html', playerHTML(), 'text/html');
});
$('#btnPrintQr').addEventListener('click', () => {
const svg = $('#qrCardSvg').innerHTML;
if (!svg) { alert('Generează mai întâi QR-ul cu butonul "Generează QR / link".'); return; }
$('#qrCard').classList.add('show');
window.print();
$('#qrCard').classList.remove('show');
});
$('#btnQrBack').addEventListener('click', () => {
$('#qrCard').classList.remove('show');
});
/* ---------- start ---------- */
renderGlobals();
renderPuzzles();
refreshPreview();
</script>
</body>
</html>