Buton "Vezi diploma" pe finale (+ "Joaca din nou"). Overlay #diploma: certificat A4 portret alb, chenar dublu accent, titlu serif (singurul), numele copilului = cel mai mare element. - buildDiploma(): rand de stele per camera (roomStars[], persistat in resume; camere sarite = 🔒 "sarita"), cuvant magic in dale (lacate pt sarite), footer = data + "creat de {creator}" + marcaj auriu "timpul a expirat" - camp builder nou: creator ("Creat de") - @media print izoleaza #diploma (rest visibility:hidden, margin 20mm, print-color-adjust:exact) - exemplu-campanie.html regenerat Smoke 31/31 (test nou "diploma": nume/titlu/stele/cuvant/creator/inapoi) + screenshot scratch/diploma.png (A4, camera sarita, footer expirat). Cluster T10/PR2 complet (D7 + Timer + Muzica + Diploma). Ramas Etapa 2: Adventure Mode v0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2577 lines
139 KiB
HTML
2577 lines
139 KiB
HTML
<!doctype html>
|
||
<html lang="ro">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Escape Room Builder</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f4f5f7; --panel: #ffffff; --ink: #1f2430; --muted: #6b7280;
|
||
--accent: #6d28d9; --accent-soft: #ede9fe; --line: #e5e7eb; --danger: #dc2626;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body { height: 100%; }
|
||
body {
|
||
margin: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||
background: var(--bg); color: var(--ink); display: flex; flex-direction: column;
|
||
}
|
||
header {
|
||
display: flex; align-items: center; gap: 12px; padding: 10px 16px;
|
||
background: var(--panel); border-bottom: 1px solid var(--line);
|
||
}
|
||
header h1 { font-size: 16px; margin: 0; font-weight: 700; }
|
||
header .spacer { flex: 1; }
|
||
button {
|
||
font: inherit; cursor: pointer; border-radius: 8px; border: 1px solid var(--line);
|
||
background: var(--panel); color: var(--ink); padding: 7px 14px;
|
||
}
|
||
button:hover { border-color: var(--accent); color: var(--accent); }
|
||
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||
button.primary:hover { filter: brightness(1.1); color: #fff; }
|
||
button.ghost { border: none; background: none; padding: 4px 6px; color: var(--muted); }
|
||
button.ghost:hover { color: var(--accent); }
|
||
button.ghost.del:hover { color: var(--danger); }
|
||
main { flex: 1; display: flex; min-height: 0; }
|
||
#editor {
|
||
width: 460px; min-width: 380px; overflow-y: auto; padding: 16px;
|
||
border-right: 1px solid var(--line); background: var(--bg);
|
||
}
|
||
#previewPane { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||
#previewPane .bar {
|
||
padding: 6px 12px; font-size: 12px; color: var(--muted);
|
||
background: var(--panel); border-bottom: 1px solid var(--line);
|
||
display: flex; align-items: center; gap: 10px;
|
||
}
|
||
iframe { flex: 1; border: 0; width: 100%; background: #1a1033; }
|
||
fieldset {
|
||
border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
|
||
padding: 12px 14px; margin: 0 0 14px;
|
||
}
|
||
legend { font-size: 12px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; padding: 0 6px; }
|
||
label { display: block; font-size: 12px; font-weight: 600; color: var(--muted); margin: 10px 0 3px; }
|
||
label.ck { display: flex; align-items: flex-start; gap: 7px; cursor: pointer; line-height: 1.4; }
|
||
label.ck input { width: auto; margin-top: 1px; flex-shrink: 0; }
|
||
label:first-of-type { margin-top: 0; }
|
||
input[type=text], textarea, select {
|
||
width: 100%; font: inherit; font-size: 14px; padding: 7px 9px;
|
||
border: 1px solid var(--line); border-radius: 7px; background: #fff; color: var(--ink);
|
||
}
|
||
input:focus, textarea:focus, select:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
|
||
textarea { resize: vertical; min-height: 54px; }
|
||
.row { display: flex; gap: 10px; }
|
||
.row > div { flex: 1; }
|
||
.row > div.narrow { flex: 0 0 110px; }
|
||
.puzzle {
|
||
border: 1px solid var(--line); border-radius: 10px; background: var(--panel); margin-bottom: 10px;
|
||
}
|
||
.puzzle > .head {
|
||
display: flex; align-items: center; gap: 6px; padding: 8px 10px; cursor: pointer; user-select: none;
|
||
}
|
||
.puzzle > .head .num {
|
||
background: var(--accent-soft); color: var(--accent); font-weight: 700; font-size: 12px;
|
||
border-radius: 6px; padding: 2px 8px;
|
||
}
|
||
.puzzle > .head .t { flex: 1; font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.puzzle > .body { padding: 0 12px 12px; border-top: 1px solid var(--line); }
|
||
.puzzle.closed > .body { display: none; }
|
||
.word {
|
||
font-family: ui-monospace, monospace; font-size: 18px; letter-spacing: 6px;
|
||
background: var(--accent-soft); color: var(--accent); border-radius: 7px;
|
||
padding: 6px 10px; display: inline-block; min-height: 32px;
|
||
}
|
||
.help { font-size: 11px; color: var(--muted); margin-top: 3px; }
|
||
#addPuzzle { width: 100%; padding: 10px; border-style: dashed; color: var(--muted); }
|
||
#addPuzzle:hover { color: var(--accent); }
|
||
input[type=color] { border: 1px solid var(--line); border-radius: 7px; height: 34px; width: 100%; padding: 2px; background: #fff; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<h1>Escape Room Builder</h1>
|
||
<div class="spacer"></div>
|
||
<button id="btnNew">Proiect nou</button>
|
||
<button id="btnLoad">Incarca JSON</button>
|
||
<button id="btnSaveJson">Salveaza JSON</button>
|
||
<button id="btnExport" class="primary">Exporta jocul HTML</button>
|
||
<input type="file" id="fileLoad" accept=".json" hidden>
|
||
</header>
|
||
|
||
<main>
|
||
<section id="editor">
|
||
<fieldset>
|
||
<legend>Joc</legend>
|
||
<div class="row">
|
||
<div>
|
||
<label>Titlul jocului</label>
|
||
<input type="text" id="gTitle" data-g="title">
|
||
</div>
|
||
<div class="narrow">
|
||
<label>Culoare</label>
|
||
<input type="color" id="gColor" data-g="color">
|
||
</div>
|
||
</div>
|
||
<div class="row">
|
||
<div>
|
||
<label>Stil joc</label>
|
||
<select id="gStyle" data-g="style">
|
||
<option value="classic">Clasic (quiz)</option>
|
||
<option value="terminal">Terminal retro</option>
|
||
<option value="arcade">Arcade pixel</option>
|
||
<option value="chat">Story chat</option>
|
||
<option value="point">Point-and-click</option>
|
||
<option value="campaign">Campanie multi-stil</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label>Personaj (Story chat)</label>
|
||
<input type="text" id="gChar" data-g="charName" placeholder="Alex">
|
||
</div>
|
||
</div>
|
||
<label>Pentru cine (optional, apare in mesaje)</label>
|
||
<input type="text" id="gPlayer" data-g="player" placeholder="ex: Paula">
|
||
<label>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 — 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 — arpegiu calm care accelereaza sub 1 minut (doar in Campanie; buton de oprire in bara)</label>
|
||
<label>Timp limita (minute, 0 = fara) — 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 — jocul continua, fara penalizare.</div>
|
||
<label>Cuvantul final (din literele puzzle-urilor)</label>
|
||
<div class="word" id="finalWord"> </div>
|
||
<div class="help">Se compune automat din campul "Litera" al fiecarui puzzle, in ordine.</div>
|
||
</fieldset>
|
||
|
||
<fieldset>
|
||
<legend>Puzzle-uri</legend>
|
||
<div id="puzzleList"></div>
|
||
<button id="addPuzzle">+ Adauga puzzle</button>
|
||
</fieldset>
|
||
</section>
|
||
|
||
<section id="previewPane">
|
||
<div class="bar">
|
||
<span>Preview live - jocul e jucabil aici, exact cum va arata exportat</span>
|
||
<div class="spacer" style="flex:1"></div>
|
||
<button id="btnReload" class="ghost">Reporneste jocul</button>
|
||
</div>
|
||
<iframe id="frame" title="Preview joc"></iframe>
|
||
</section>
|
||
</main>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
/* ---------- stare ---------- */
|
||
|
||
const STORAGE_KEY = 'escape-builder-v1';
|
||
|
||
const CAMPAIGN_ROTATION = ['classic', 'terminal', 'arcade', 'chat', 'point'];
|
||
const CAMPAIGN_STYLE_NAMES = { classic: 'Clasic', terminal: 'Terminal Retro', arcade: 'Arcade Pixel', chat: 'Story Chat', point: 'Point-and-Click' };
|
||
/* 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,
|
||
timerMin: 0,
|
||
story: 'O comoara a fost ascunsa, iar singurul drum spre ea trece prin cateva incercari. Rezolva fiecare puzzle ca sa aduni literele cuvantului magic.',
|
||
finalMessage: 'Felicitari! Ai gasit comoara!',
|
||
puzzles: [
|
||
{ title: 'Incalzirea', type: 'free', question: 'Cat fac 7 x 8?', answer: '56', tfAnswer: 'Adevarat', choices: '', hint: 'Tabla inmultirii cu 7.', letter: 'D' },
|
||
{ title: 'Adevarat sau fals', type: 'tf', question: 'Romania are iesire la Marea Neagra.', answer: '', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'A' },
|
||
{ title: 'Alege raspunsul', type: 'choice', question: 'Care este capitala Frantei?', answer: '', tfAnswer: 'Adevarat', choices: '*Paris\nLyon\nMarsilia', hint: 'Turnul Eiffel.', letter: 'R' }
|
||
]
|
||
});
|
||
|
||
function normalizePuzzle(p) {
|
||
const validTypes = ['free', 'tf', 'choice'];
|
||
const validStyles = ['', 'classic', 'terminal', 'arcade', 'chat', 'point'];
|
||
if (typeof p.title !== 'string') p.title = '';
|
||
if (!validTypes.includes(p.type)) p.type = 'free';
|
||
if (typeof p.question !== 'string') p.question = '';
|
||
if (typeof p.answer !== 'string') p.answer = '';
|
||
if (p.tfAnswer !== 'Adevarat' && p.tfAnswer !== 'Fals') p.tfAnswer = 'Adevarat';
|
||
if (typeof p.choices !== 'string') p.choices = '';
|
||
if (typeof p.hint !== 'string') p.hint = '';
|
||
if (typeof p.letter !== 'string') p.letter = '';
|
||
if (!validStyles.includes(p.style || '')) p.style = '';
|
||
if (typeof p.style === 'undefined') p.style = '';
|
||
return p;
|
||
}
|
||
|
||
const blankPuzzle = () => normalizePuzzle({ title: '', type: 'free', question: '', answer: '', tfAnswer: 'Adevarat', choices: '', hint: '', letter: '', style: '' });
|
||
|
||
let state = Object.assign(defaultState(), load() || {});
|
||
if (!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 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">▲</button>
|
||
<button class="ghost" data-act="down" title="Muta jos">▼</button>
|
||
<button class="ghost del" data-act="del" title="Sterge">✕</button>
|
||
</div>
|
||
<div class="body">
|
||
<div class="row">
|
||
<div>
|
||
<label>Titlu scurt</label>
|
||
<input type="text" data-f="title" value="${esc(p.title)}">
|
||
</div>
|
||
<div class="narrow">
|
||
<label>Tip</label>
|
||
<select data-f="type">
|
||
<option value="free" ${p.type === 'free' ? 'selected' : ''}>Raspuns liber</option>
|
||
<option value="tf" ${p.type === 'tf' ? 'selected' : ''}>Adevarat / Fals</option>
|
||
<option value="choice" ${p.type === 'choice' ? 'selected' : ''}>Variante</option>
|
||
</select>
|
||
</div>
|
||
${state.style === 'campaign' ? `
|
||
<div class="narrow">
|
||
<label>Stil cameră</label>
|
||
<select data-f="style">
|
||
<option value="" ${!p.style ? 'selected' : ''}>Auto (${esc(CAMPAIGN_STYLE_NAMES[CAMPAIGN_ROTATION[i % CAMPAIGN_ROTATION.length]])})</option>
|
||
<option value="classic" ${p.style === 'classic' ? 'selected' : ''}>Clasic</option>
|
||
<option value="terminal" ${p.style === 'terminal' ? 'selected' : ''}>Terminal Retro</option>
|
||
<option value="arcade" ${p.style === 'arcade' ? 'selected' : ''}>Arcade Pixel</option>
|
||
<option value="chat" ${p.style === 'chat' ? 'selected' : ''}>Story Chat</option>
|
||
<option value="point" ${p.style === 'point' ? 'selected' : ''}>Point-and-Click</option>
|
||
</select>
|
||
</div>` : ''}
|
||
</div>
|
||
${state.style === 'campaign' && p.style && !CAMPAIGN_STYLE_NAMES[p.style] ? `<div class="help" style="color:#f87171">Stil necunoscut "${esc(p.style)}" — se foloseste Auto</div>` : ''}
|
||
<label>Intrebarea</label>
|
||
<textarea data-f="question" rows="2">${esc(p.question)}</textarea>
|
||
${p.type === 'free' ? `
|
||
<label>Raspunsul corect</label>
|
||
<input type="text" data-f="answer" value="${esc(p.answer)}">
|
||
<div class="help">Nu conteaza literele mari/mici sau diacriticele.</div>` : ''}
|
||
${p.type === 'tf' ? `
|
||
<label>Raspunsul corect</label>
|
||
<select data-f="tfAnswer">
|
||
<option ${p.tfAnswer === 'Adevarat' ? 'selected' : ''}>Adevarat</option>
|
||
<option ${p.tfAnswer === 'Fals' ? 'selected' : ''}>Fals</option>
|
||
</select>` : ''}
|
||
${p.type === 'choice' ? `
|
||
<label>Variante (una pe linie, pune * inaintea celei corecte)</label>
|
||
<textarea data-f="choices" rows="3">${esc(p.choices)}</textarea>` : ''}
|
||
<div class="row">
|
||
<div>
|
||
<label>Indiciu (optional)</label>
|
||
<input type="text" data-f="hint" value="${esc(p.hint)}">
|
||
</div>
|
||
<div class="narrow">
|
||
<label>Litera</label>
|
||
<input type="text" data-f="letter" maxlength="1" value="${esc(p.letter)}">
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
return div;
|
||
}
|
||
|
||
function renderPuzzles() {
|
||
puzzleList.innerHTML = '';
|
||
state.puzzles.forEach((p, i) => puzzleList.appendChild(puzzleCard(p, i)));
|
||
}
|
||
|
||
function esc(s) {
|
||
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
/* evenimente editor */
|
||
|
||
document.querySelectorAll('[data-g]').forEach(el => {
|
||
el.addEventListener('input', () => {
|
||
state[el.dataset.g] = el.value;
|
||
if (el.dataset.g === 'style') renderPuzzles(); /* re-render: style selector per card apare/dispare */
|
||
onChange();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-gb]').forEach(el => {
|
||
el.addEventListener('change', () => { state[el.dataset.gb] = el.checked; onChange(); });
|
||
});
|
||
|
||
puzzleList.addEventListener('input', e => {
|
||
const f = e.target.dataset.f;
|
||
if (!f) return;
|
||
const i = +e.target.closest('.puzzle').dataset.i;
|
||
state.puzzles[i][f] = e.target.value;
|
||
if (f === 'title' || f === 'question') {
|
||
const card = e.target.closest('.puzzle');
|
||
card.querySelector('.head .t').textContent = state.puzzles[i].title || state.puzzles[i].question || 'Puzzle fara titlu';
|
||
}
|
||
if (f === 'type') { state.puzzles[i]._closed = false; renderPuzzles(); }
|
||
onChange();
|
||
});
|
||
|
||
puzzleList.addEventListener('click', e => {
|
||
const btn = e.target.closest('button[data-act]');
|
||
if (btn) {
|
||
const card = btn.closest('.puzzle');
|
||
const i = +card.dataset.i;
|
||
const act = btn.dataset.act;
|
||
if (act === 'del') {
|
||
if (!confirm('Stergi puzzle-ul ' + (i + 1) + '?')) return;
|
||
state.puzzles.splice(i, 1);
|
||
}
|
||
if (act === 'up' && i > 0) [state.puzzles[i - 1], state.puzzles[i]] = [state.puzzles[i], state.puzzles[i - 1]];
|
||
if (act === 'down' && i < state.puzzles.length - 1) [state.puzzles[i + 1], state.puzzles[i]] = [state.puzzles[i], state.puzzles[i + 1]];
|
||
renderPuzzles(); onChange();
|
||
return;
|
||
}
|
||
const head = e.target.closest('.head');
|
||
if (head && !e.target.closest('input,select,textarea')) {
|
||
const card = head.closest('.puzzle');
|
||
const i = +card.dataset.i;
|
||
state.puzzles[i]._closed = !state.puzzles[i]._closed;
|
||
card.classList.toggle('closed');
|
||
}
|
||
});
|
||
|
||
$('#addPuzzle').addEventListener('click', () => {
|
||
state.puzzles.forEach(p => p._closed = true);
|
||
state.puzzles.push(blankPuzzle());
|
||
renderPuzzles(); onChange();
|
||
puzzleList.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
puzzleList.lastElementChild.querySelector('[data-f=title]').focus();
|
||
});
|
||
|
||
$('#btnNew').addEventListener('click', () => {
|
||
if (!confirm('Incepi un proiect nou? Proiectul curent se pierde (salveaza-l mai intai ca JSON daca vrei sa-l pastrezi).')) return;
|
||
state = defaultState();
|
||
renderGlobals(); renderPuzzles(); onChange();
|
||
});
|
||
|
||
$('#btnSaveJson').addEventListener('click', () => {
|
||
download(slug(state.title) + '.json', JSON.stringify(cleanState(), null, 2), 'application/json');
|
||
});
|
||
|
||
$('#btnLoad').addEventListener('click', () => $('#fileLoad').click());
|
||
$('#fileLoad').addEventListener('change', e => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
file.text().then(txt => {
|
||
try {
|
||
const data = JSON.parse(txt);
|
||
if (!Array.isArray(data.puzzles)) throw new Error('format');
|
||
state = Object.assign(defaultState(), data);
|
||
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));
|
||
s.timerMin = Math.max(0, Math.min(120, parseInt(s.timerMin, 10) || 0)); /* T10: minute întregi 0..120 */
|
||
s.puzzles.forEach(p => {
|
||
delete p._closed;
|
||
/* D13: letter normalizat la 1 caracter alfanumeric (bug: un < strică scena SVG din point) */
|
||
p.letter = (p.letter || '').replace(/[^A-Za-z0-9]/g, '').charAt(0);
|
||
});
|
||
return s;
|
||
}
|
||
|
||
function slug(s) {
|
||
return (s || 'escape-room').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
|
||
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'escape-room';
|
||
}
|
||
|
||
function download(name, content, mime) {
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(new Blob([content], { type: mime }));
|
||
a.download = name;
|
||
a.click();
|
||
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
||
}
|
||
|
||
/* ---------- preview ---------- */
|
||
|
||
let timer = null;
|
||
function onChange() {
|
||
persist(); renderWord();
|
||
clearTimeout(timer);
|
||
timer = setTimeout(refreshPreview, 400);
|
||
}
|
||
|
||
function refreshPreview() {
|
||
if (state.puzzles.length === 0) {
|
||
$('#frame').srcdoc = '<html><body style="font:system-ui,sans-serif;color:#fff;background:#0d0620;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;text-align:center;padding:20px"><div><div style="font-size:48px;margin-bottom:16px">🚪</div><p style="opacity:.7;line-height:1.6;margin:0">Adaugă cel puțin un puzzle<br>ca să vezi preview-ul.</p></div></body></html>';
|
||
return;
|
||
}
|
||
const previewCfg = cleanState();
|
||
if (previewCfg.style === 'campaign') previewCfg._noResume = true; /* preview nu reia niciodată (D3) */
|
||
$('#frame').srcdoc = gameHTML(previewCfg);
|
||
}
|
||
|
||
/* ---------- template-urile jocului exportat ---------- */
|
||
|
||
function gameHTML(cfg) {
|
||
const engines = { classic: gameClassic, terminal: gameTerminal, arcade: gameArcade, chat: gameChat, point: gamePoint, campaign: gameCampaign };
|
||
return (engines[cfg.style] || gameClassic)(cfg);
|
||
}
|
||
|
||
function gameClassic(cfg) {
|
||
/* CFG + 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 ''; }
|
||
function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type === 'choice' ? choiceCorrect(p) : p.answer); return norm(given) !== '' && norm(given) === norm(exp); }
|
||
function beep(ok){ if(CFG._campaign){ try{ parent.beep(ok); }catch(e){} return; } try { var ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)()); var t = ctx.currentTime; var fs = ok ? [523, 784] : [196]; fs.forEach(function(f, k){ var o = ctx.createOscillator(), g = ctx.createGain(); o.frequency.value = f; o.type = 'triangle'; g.gain.setValueAtTime(0.12, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.001, t + k * 0.09 + 0.25); o.connect(g); g.connect(ctx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.3); }); } catch (e) {} }
|
||
function confetti(){ var colors = [CFG.color || '#6d28d9', '#fbbf24', '#34d399', '#60a5fa', '#f472b6']; for (var i = 0; i < 90; i++){ var c = document.createElement('div'); c.className = 'confetti'; c.style.left = (i * 137 % 100) + 'vw'; c.style.background = colors[i % colors.length]; c.style.animationDuration = (2.2 + (i * 53 % 18) / 10) + 's'; c.style.animationDelay = ((i * 31 % 14) / 10) + 's'; document.body.appendChild(c); } }
|
||
function roomReady(){ if(CFG._campaign){ try{ parent.roomReady(CFG._campaign.idx); }catch(e){} } }
|
||
/* Contract de finalizare a camerei — un singur loc pentru payload-ul parent.nextRoom
|
||
(înlocuiește duplicatele din showFinal/finale; D7). Citește totalStars + finalWord() la apel. */
|
||
function campaignDone(){ if(CFG._campaign){ try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:finalWord().charAt(0)}); }catch(e){} } }
|
||
window.onerror = function(msg){ if(CFG._campaign){ try{ parent.roomError(CFG._campaign.idx, String(msg)); }catch(e){} } };
|
||
if(CFG._campaign){
|
||
/* Mod cameră (§Design pct.12): ascunde h1, progres propriu, restart propriu */
|
||
var _cs = document.createElement('style');
|
||
_cs.textContent = 'h1{display:none!important}.progress{display:none!important}.meta{display:none!important}';
|
||
(document.head || document.documentElement).appendChild(_cs);
|
||
}`;
|
||
}
|
||
|
||
const SNIP = {};
|
||
|
||
SNIP.baseCss = `
|
||
.confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 99; animation: fall linear forwards; }
|
||
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
|
||
.shake { animation: shake .4s ease; }
|
||
@keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }
|
||
@media (prefers-reduced-motion: reduce){ .confetti{ display:none !important; } .shake{ animation:none !important; } }`;
|
||
|
||
SNIP.modalCss = `
|
||
#mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; }
|
||
#mCard { width: 100%; max-width: 460px; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 22px; color: #fff; font-family: system-ui, sans-serif; box-shadow: 0 18px 50px rgba(0,0,0,.5); }
|
||
#mCard .mtitle { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #c4b5fd; font-weight: 700; }
|
||
#mCard .mq { font-size: 18px; line-height: 1.45; margin: 8px 0 16px; }
|
||
#mCard input[type=text] { width: 100%; font: inherit; font-size: 17px; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(255,255,255,.25); background: rgba(0,0,0,.3); color: #fff; text-align: center; box-sizing: border-box; }
|
||
#mCard input:focus { outline: 2px solid var(--accent); border-color: transparent; }
|
||
#mCard button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 11px 16px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; margin-top: 10px; box-sizing: border-box; }
|
||
#mCard button.opt { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); font-weight: 600; text-align: left; }
|
||
#mCard button.opt:hover { background: rgba(255,255,255,.2); }
|
||
#mCard .mfb { min-height: 20px; text-align: center; font-weight: 700; margin-top: 10px; }
|
||
#mCard .mfb.bad { color: #fda4af; } #mCard .mfb.good { color: #86efac; }
|
||
#mCard .mhint { background: none !important; color: rgba(255,255,255,.55) !important; font-weight: 600 !important; font-size: 13px; width: auto !important; display: block; margin: 10px auto 0; }
|
||
#mCard .mhinttext { background: rgba(255,255,255,.1); border-radius: 9px; padding: 9px 11px; font-size: 14px; margin-top: 8px; display: none; white-space: pre-line; }
|
||
#mCard .mclose { background: none !important; color: rgba(255,255,255,.4) !important; font-size: 12px; width: auto !important; margin: 6px auto 0; display: block; }`;
|
||
|
||
SNIP.modalHtml = `<div id="mOverlay"><div id="mCard">
|
||
<div class="mtitle" id="mTitle"></div>
|
||
<div class="mq" id="mQ"></div>
|
||
<div id="mAnswers"></div>
|
||
<div class="mfb" id="mFeedback"></div>
|
||
<button class="mhint" id="mHintBtn">Vreau un indiciu</button>
|
||
<div class="mhinttext" id="mHintText"></div>
|
||
<button class="mclose" id="mClose">Pleaca de aici</button>
|
||
</div></div>`;
|
||
|
||
SNIP.modalJs = `var mIdx = -1, mAtt = 0, mHint = false, mCb = null;
|
||
el('mHintBtn').onclick = function(){ mHint = true; el('mHintText').style.display = 'block'; };
|
||
el('mClose').onclick = function(){ el('mOverlay').style.display = 'none'; };
|
||
function modalOpen(){ return el('mOverlay').style.display === 'flex'; }
|
||
function openPuzzle(i, cb){
|
||
mIdx = i; mAtt = 0; mHint = false; mCb = cb;
|
||
var p = CFG.puzzles[i];
|
||
el('mTitle').textContent = p.title || ('Puzzle ' + (i + 1));
|
||
el('mQ').textContent = p.question;
|
||
el('mFeedback').textContent = ''; el('mFeedback').className = 'mfb';
|
||
el('mHintText').style.display = 'none'; el('mHintText').textContent = p.hint || '';
|
||
el('mHintBtn').style.display = p.hint ? '' : 'none';
|
||
var box = el('mAnswers'); box.innerHTML = '';
|
||
if (p.type === 'free') {
|
||
var inp = document.createElement('input'); inp.type = 'text'; inp.placeholder = 'Scrie raspunsul...'; inp.autocomplete = 'off';
|
||
var b = document.createElement('button'); b.textContent = 'Verifica';
|
||
b.onclick = function(){ mCheck(inp.value); };
|
||
inp.onkeydown = function(e){ e.stopPropagation(); if (e.key === 'Enter') b.click(); };
|
||
box.appendChild(inp); box.appendChild(b);
|
||
setTimeout(function(){ inp.focus(); }, 60);
|
||
} else if (p.type === 'tf') {
|
||
['Adevarat', 'Fals'].forEach(function(v){ var b = document.createElement('button'); b.className = 'opt'; b.textContent = v; b.onclick = function(){ mCheck(v); }; box.appendChild(b); });
|
||
} else {
|
||
choiceOpts(p).forEach(function(o){ var b = document.createElement('button'); b.className = 'opt'; b.textContent = o; b.onclick = function(){ mCheck(o); }; box.appendChild(b); });
|
||
}
|
||
el('mOverlay').style.display = 'flex';
|
||
}
|
||
function mCheck(given){
|
||
var p = CFG.puzzles[mIdx];
|
||
if (checkAnswer(p, given)) {
|
||
var s = starsFor(mAtt, mHint);
|
||
totalStars += s; beep(true);
|
||
el('mFeedback').textContent = 'Corect! +' + s + ' \\u2605'; el('mFeedback').className = 'mfb good';
|
||
setTimeout(function(){ el('mOverlay').style.display = 'none'; var cb = mCb; mCb = null; if (cb) cb(mIdx, s); }, 750);
|
||
} else {
|
||
mAtt++; beep(false);
|
||
el('mFeedback').textContent = 'Nu e bine, mai incearca!'; el('mFeedback').className = 'mfb bad';
|
||
var c = el('mCard'); c.classList.remove('shake'); void c.offsetWidth; c.classList.add('shake');
|
||
}
|
||
}`;
|
||
|
||
SNIP.finalCss = `
|
||
#fOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.88); z-index: 30; align-items: center; justify-content: center; padding: 16px; }
|
||
#fOverlay .fcard { width: 100%; max-width: 480px; text-align: center; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 18px; padding: 28px; color: #fff; font-family: system-ui, sans-serif; }
|
||
#fOverlay h1 { margin: 0 0 8px; font-size: 26px; }
|
||
#fOverlay .fstars { font-size: 26px; letter-spacing: 4px; color: #fbbf24; margin: 6px 0; }
|
||
#fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; }
|
||
#fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; }
|
||
@keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
|
||
@media (prefers-reduced-motion: reduce){ #fOverlay .fword span{ animation:none !important; } }
|
||
#fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; }
|
||
#fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }`;
|
||
|
||
SNIP.finalHtml = `<div id="fOverlay"><div class="fcard">
|
||
<h1>Evadare reusita!</h1>
|
||
<div class="fstars" id="fStars"></div>
|
||
<div class="fword" id="fWord"></div>
|
||
<p id="fMsg"></p>
|
||
<button id="fAgain">Joaca din nou</button>
|
||
</div></div>`;
|
||
|
||
SNIP.finalJs = `function showFinal(){
|
||
if(CFG._campaign){ campaignDone(); return; }
|
||
el('fStars').textContent = totalStars + ' / ' + (CFG.puzzles.length * 3) + ' \\u2605';
|
||
var w = finalWord(); var bw = el('fWord'); bw.innerHTML = '';
|
||
for (var j = 0; j < w.length; j++){ var s = document.createElement('span'); s.textContent = w.charAt(j); s.style.animationDelay = (j * 0.18) + 's'; bw.appendChild(s); }
|
||
var msg = CFG.finalMessage || '';
|
||
el('fMsg').textContent = CFG.player ? CFG.player + ', ' + msg.charAt(0).toLowerCase() + msg.slice(1) : msg;
|
||
el('fOverlay').style.display = 'flex';
|
||
beep(true); confetti();
|
||
}
|
||
el('fAgain').onclick = function(){ location.reload(); };`;
|
||
|
||
/* 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);
|
||
}
|
||
}`;
|
||
|
||
/* ---------- 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>></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 💣 = bomba. Sparge cutiile (uneori cad bonusuri: 🔥 raza, 💣 bombe in plus), evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.</div>
|
||
<div id="dpad"><button data-d="L" aria-label="Stanga">◀</button><button data-d="U" aria-label="Sus">▲</button><button data-d="D" aria-label="Jos">▼</button><button data-d="R" aria-label="Dreapta">▶</button><button id="btnBomb" aria-label="Pune bomba">💣</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 gameCampaign(cfg) {
|
||
const ROTATION = ['classic', 'terminal', 'arcade', 'chat', 'point'];
|
||
|
||
/* Template-uri per stil: fiecare motor generat O dată cu sentinel __CFG__ */
|
||
const stylesNeeded = new Set(cfg.puzzles.map((p, i) => p.style || ROTATION[i % ROTATION.length]));
|
||
const engines = { classic: gameClassic, terminal: gameTerminal, arcade: gameArcade, chat: gameChat, point: gamePoint };
|
||
const templates = {};
|
||
for (const style of stylesNeeded) {
|
||
templates[style] = (engines[style] || gameClassic)('__TEMPLATE__');
|
||
}
|
||
|
||
const tplJson = JSON.stringify(templates).replace(/</g, '\\u003c');
|
||
const masterJson = JSON.stringify(cfg).replace(/</g, '\\u003c');
|
||
const nRooms = cfg.puzzles.length;
|
||
const nStyles = stylesNeeded.size;
|
||
|
||
return `<!doctype html>
|
||
<html lang="ro">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>${esc(cfg.title)}</title>
|
||
<style>
|
||
/*
|
||
* ASCII DIAGRAM — contractul parent.*:
|
||
* ┌──────────── orchestrator (window) ─────────────────────────┐
|
||
* │ nextRoom({idx,stars,letter}) ← camera.parent.nextRoom │
|
||
* │ roomReady(idx) ← camera.parent.roomReady │
|
||
* │ roomError(idx,msg) ← camera.parent.roomError │
|
||
* │ beep(ok) ← camera.parent.beep │
|
||
* └────────────────────────────────────────────────────────────┘
|
||
*/
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; height: 100%; overflow: hidden; }
|
||
body {
|
||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||
background: var(--c-bg, #0d0620); color: var(--c-ink, #fff);
|
||
display: flex; flex-direction: column;
|
||
--c-bg: #0d0620; --c-surface: #221440; --c-line: rgba(255,255,255,.18);
|
||
--c-ink: #fff; --c-gold: #fbbf24;
|
||
}
|
||
#chrome {
|
||
height: 48px; min-height: 48px; flex-shrink: 0;
|
||
background: #1a0e3a; border-bottom: 1px solid rgba(255,255,255,.15);
|
||
display: flex; align-items: center; padding: 0 16px; gap: 12px;
|
||
}
|
||
#chrome-title { font-size: 15px; font-weight: 700; }
|
||
#chrome .sp { flex: 1; }
|
||
/* 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-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">${esc(cfg.title)}</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>🎵</button>
|
||
<button id="btn-voice" type="button" aria-label="Naratiune vocala" hidden>🔊</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">▲</button><button class="sp" aria-hidden="true" tabindex="-1"></button>
|
||
<button data-d="L" aria-label="Stanga">◀</button><button class="sp" aria-hidden="true" tabindex="-1"></button><button data-d="R" aria-label="Dreapta">▶</button>
|
||
<button class="sp" aria-hidden="true" tabindex="-1"></button><button data-d="D" aria-label="Jos">▼</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 →</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">← Înapoi</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/*
|
||
* ASCII DIAGRAM — contractul parent.*:
|
||
* ┌──────────── orchestrator (window) ─────────────────────────┐
|
||
* │ nextRoom({idx,stars,letter}) ← camera.parent.nextRoom │
|
||
* │ roomReady(idx) ← camera.parent.roomReady │
|
||
* │ roomError(idx,msg) ← camera.parent.roomError │
|
||
* │ beep(ok) ← camera.parent.beep │
|
||
* └────────────────────────────────────────────────────────────┘
|
||
* Idempotență: nextRoom/roomError acceptate doar de la activeWindow.
|
||
* roomDone[idx]=true după primul nextRoom → duplicatele ignorate.
|
||
* Timeout 4s → skipRoom → aceeași compoziție de coridor (D5).
|
||
*/
|
||
var TPL = ${tplJson};
|
||
var MASTER = ${masterJson};
|
||
var ROTATION = ['classic','terminal','arcade','chat','point'];
|
||
var TOKEN = '__CFG__';
|
||
|
||
document.documentElement.style.setProperty('--accent', MASTER.color || '#6d28d9');
|
||
|
||
var N = MASTER.puzzles.length;
|
||
var totalStars = 0;
|
||
var collected = [];
|
||
var roomStars = []; /* stele per cameră — pentru diplomă (§Design pct.9) */
|
||
var skipped = {};
|
||
|
||
var activeIdx = -1;
|
||
var activeWindow = null;
|
||
var readyTimer = null;
|
||
var roomDone = {};
|
||
|
||
/* ----- Resume — safeStore (D3) + djb2 hash (D11) ----- */
|
||
function djb2(s){
|
||
var h = 5381;
|
||
for(var i=0;i<s.length;i++) h = ((h<<5)+h) ^ s.charCodeAt(i);
|
||
return (h >>> 0).toString(36);
|
||
}
|
||
var _RESUME_KEY = 'esc-camp-' + djb2(JSON.stringify(MASTER));
|
||
function safeGet(){ try{ return JSON.parse(sessionStorage.getItem(_RESUME_KEY)); }catch(e){ return null; } }
|
||
function safeSet(v){ try{ sessionStorage.setItem(_RESUME_KEY, JSON.stringify(v)); }catch(e){ /* webview restrictat — continuă fără resume */ } }
|
||
function saveProgress(){
|
||
safeSet({ idx: activeIdx, totalStars: totalStars, collected: collected.slice(), roomStars: roomStars.slice(), skipped: skipped });
|
||
}
|
||
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">🔒</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">▶</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 ----- */
|
||
|
||
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');
|
||
saveProgress();
|
||
console.log('[campaign] camera',idx,'done. stars=',data.stars,'letter=',letter);
|
||
var next = idx + 1;
|
||
if(next >= N){ clearProgress(); showFinale(); } else { showOverworld(next, data); }
|
||
};
|
||
|
||
window.roomReady = function(idx){
|
||
console.log('[campaign] roomReady',idx);
|
||
if(+idx !== activeIdx) return;
|
||
clearTimeout(readyTimer);
|
||
frameEl.setAttribute('data-room-ready','true'); /* hook pentru tests */
|
||
var q = MASTER.puzzles[idx] && MASTER.puzzles[idx].question;
|
||
if(q) voiceSay(q); /* citește întrebarea camerei (D10) */
|
||
};
|
||
|
||
window.roomError = function(idx, msg){
|
||
console.warn('[campaign] roomError',idx,msg);
|
||
/* roomError are semantică ORICÂND (post-ready inclus, D5) */
|
||
if(!activeWindow || frameEl.contentWindow !== activeWindow) return;
|
||
if(+idx !== activeIdx) return;
|
||
if(roomDone[idx]) return;
|
||
skipRoom(+idx, String(msg||'eroare'));
|
||
};
|
||
|
||
/* ----- Timeout 4s → skip (T3) ----- */
|
||
function startReadyTimer(idx){
|
||
clearTimeout(readyTimer);
|
||
readyTimer = setTimeout(function(){
|
||
if(roomDone[idx]) return;
|
||
console.warn('[campaign] timeout 4s — skip',idx);
|
||
skipRoom(idx,'timeout');
|
||
}, 4000);
|
||
}
|
||
|
||
function skipRoom(idx, reason){
|
||
clearTimeout(readyTimer);
|
||
roomDone[idx] = true;
|
||
skipped[idx] = true;
|
||
setDot(idx,'done');
|
||
saveProgress();
|
||
var style = (MASTER.puzzles[idx]&&(MASTER.puzzles[idx].style||ROTATION[idx%5]))||'?';
|
||
var code = style + '\\u00b7' + idx;
|
||
showSkipBanner(idx, code, reason);
|
||
}
|
||
|
||
/* ----- Montare cameră ----- */
|
||
function mountRoom(idx){
|
||
activeIdx = idx;
|
||
var puzzle = MASTER.puzzles[idx];
|
||
var style = (puzzle&&puzzle.style) || ROTATION[idx % ROTATION.length];
|
||
var tpl = TPL[style];
|
||
if(!tpl){
|
||
/* stil negăsit în template-uri — skip imediat */
|
||
console.warn('[campaign] template lipsă pentru stil',style);
|
||
skipRoom(idx,'template lipsă: '+style); return;
|
||
}
|
||
var camCfg = {
|
||
title: MASTER.title, player: MASTER.player, color: MASTER.color,
|
||
style: style, charName: MASTER.charName,
|
||
story: MASTER.story, finalMessage: MASTER.finalMessage,
|
||
puzzles: [puzzle],
|
||
_campaign: {
|
||
idx: idx, total: N,
|
||
stars: totalStars, letters: collected.slice(), deadline: null
|
||
}
|
||
};
|
||
/* json cu replace-funcție (D1 + D6) */
|
||
var json = JSON.stringify(camCfg).replace(/</g,'\\u003c');
|
||
var srcdoc = tpl.replace(TOKEN, function(){ return json; });
|
||
|
||
hideAll();
|
||
setDot(idx,'active');
|
||
activeWindow = null;
|
||
frameEl.removeAttribute('data-room-ready');
|
||
frameEl.srcdoc = srcdoc;
|
||
setTimeout(function(){ activeWindow = frameEl.contentWindow; }, 0);
|
||
startReadyTimer(idx);
|
||
|
||
var isLast = (idx === N - 1);
|
||
document.getElementById('chrome-title').textContent = isLast
|
||
? MASTER.title + ' — Ultima cameră!' : MASTER.title;
|
||
console.log('[campaign] montat camera',idx,'stil',style);
|
||
}
|
||
|
||
/* ----- Skip banner ----- */
|
||
function showSkipBanner(idx, code, reason){
|
||
hideAll();
|
||
var stuckStyle = (MASTER.puzzles[idx] && (MASTER.puzzles[idx].style || ROTATION[idx%5])) || 'classic';
|
||
document.getElementById('skip-door').innerHTML = doorHtml(stuckStyle, false, true);
|
||
document.getElementById('skip-code').textContent = 'Cod: ' + code + ' (' + reason + ')';
|
||
skipEl.classList.add('show');
|
||
var next = idx + 1;
|
||
var btn = document.getElementById('btn-skip');
|
||
btn.disabled = false;
|
||
btn.onclick = function(){
|
||
btn.disabled = true;
|
||
if(next >= N){ showFinale(); } else { showOverworld(next); }
|
||
};
|
||
}
|
||
|
||
/* ----- Final ----- */
|
||
function showFinale(){
|
||
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 { 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];
|
||
el.className = 'ow-door' + (done ? ' solved' : '') + (!done && d.idx === owTargetIdx ? ' target' : '');
|
||
if (isSkip) el.textContent = '\\ud83d\\udd12';
|
||
else if (done) el.textContent = (MASTER.puzzles[d.idx].letter || '').trim().toUpperCase() || '\\u2713';
|
||
else el.textContent = (d.idx + 1);
|
||
});
|
||
var ex = document.getElementById('ow-exit'); if (ex) ex.className = 'ow-exit' + (owAllDone() ? ' open' : '');
|
||
document.getElementById('ow-hint').textContent = owAllDone()
|
||
? 'Toate camerele rezolvate! Mergi la steag \\ud83c\\udfc1 ca să evadezi.'
|
||
: 'Mergi la ușa următoare (săgeți / WASD / butoane).';
|
||
}
|
||
|
||
function owCenter(){
|
||
var vpW = owWrap.clientWidth, vpH = owWrap.clientHeight;
|
||
var worldW = OW_COLS * OW_TILE, worldH = OW_ROWS * OW_TILE;
|
||
var px = owPlayer.col * OW_TILE + OW_TILE / 2, py = owPlayer.row * OW_TILE + OW_TILE / 2;
|
||
var tx = worldW <= vpW ? (vpW - worldW) / 2 : Math.max(vpW - worldW, Math.min(0, vpW / 2 - px));
|
||
var ty = worldH <= vpH ? (vpH - worldH) / 2 : Math.max(vpH - worldH, Math.min(0, vpH / 2 - py));
|
||
owWorld.style.transform = 'translate(' + tx + 'px,' + ty + 'px)';
|
||
}
|
||
|
||
function owRenderPlayer(){ if (owPlayerEl){ owPlayerEl.style.left = (owPlayer.col * OW_TILE) + 'px'; owPlayerEl.style.top = (owPlayer.row * OW_TILE) + 'px'; } owCenter(); }
|
||
|
||
function owWalkable(col, row){ if (col < 0 || row < 0 || col >= OW_COLS || row >= OW_ROWS) return false; return owMap[row][col] !== 1; }
|
||
|
||
function owMove(dc, dr){
|
||
if (!owActive) return;
|
||
var nc = owPlayer.col + dc, nr = owPlayer.row + dr;
|
||
if (!owWalkable(nc, nr)) return;
|
||
owPlayer.col = nc; owPlayer.row = nr; owRenderPlayer(); owCheckEnter();
|
||
}
|
||
|
||
function owCheckEnter(){
|
||
for (var i = 0; i < owDoors.length; i++){ var d = owDoors[i]; if (owPlayer.col === d.col && owPlayer.row === d.row){ if (!roomDone[d.idx]) owEnterDoor(d.idx); return; } }
|
||
if (owPlayer.col === owExit.col && owPlayer.row === owExit.row && owAllDone()){ owActive = false; showFinale(); }
|
||
}
|
||
|
||
function owEnterDoor(idx){ if (!owActive) return; /* idempotență — a doua intrare ignorată (T4/D4) */ owActive = false; mountRoom(idx); }
|
||
|
||
function showOverworld(targetIdx, data){
|
||
hideAll();
|
||
owTargetIdx = targetIdx;
|
||
owRefreshDoors();
|
||
owRenderPlayer();
|
||
owActive = true;
|
||
overworldEl.classList.add('show');
|
||
if (data){
|
||
var s = data.stars || 0;
|
||
var letter = String(data.letter || '').replace(/[^A-Za-z0-9]/g, '').charAt(0).toUpperCase();
|
||
var t = (letter ? ('+' + letter + ' ') : '') + (s ? ('+' + s + ' \\u2605') : '');
|
||
var toast = document.getElementById('ow-toast');
|
||
if (t.trim()){ toast.textContent = t; toast.classList.add('show'); setTimeout(function(){ toast.classList.remove('show'); }, 1600); }
|
||
}
|
||
setTimeout(owCenter, 0);
|
||
}
|
||
|
||
document.addEventListener('keydown', function(e){
|
||
if (!owActive) return;
|
||
var m = { ArrowUp:[0,-1], ArrowDown:[0,1], ArrowLeft:[-1,0], ArrowRight:[1,0], w:[0,-1], s:[0,1], a:[-1,0], d:[1,0] }[e.key];
|
||
if (!m) return; e.preventDefault(); owMove(m[0], m[1]);
|
||
});
|
||
document.querySelectorAll('#ow-dpad button[data-d]').forEach(function(b){
|
||
b.addEventListener('click', function(){ var m = { U:[0,-1], D:[0,1], L:[-1,0], R:[1,0] }[b.getAttribute('data-d')]; if (m) owMove(m[0], m[1]); });
|
||
});
|
||
|
||
/* Hooks pentru teste (conduc harta fără tastatură) */
|
||
window.__ow = {
|
||
get state(){ return { player: { col: owPlayer.col, row: owPlayer.row }, target: owTargetIdx, active: owActive, allDone: owAllDone(), doors: owDoors.map(function(d){ return { idx: d.idx, col: d.col, row: d.row, solved: !!roomDone[d.idx] }; }) }; },
|
||
enterDoor: function(i){ var d = owDoors[i]; if (d){ owPlayer.col = d.col; owPlayer.row = d.row; owRenderPlayer(); owCheckEnter(); } },
|
||
enterExit: function(){ owPlayer.col = owExit.col; owPlayer.row = owExit.row; owRenderPlayer(); owCheckEnter(); }
|
||
};
|
||
|
||
owBuild();
|
||
|
||
/* ----- Intro ----- */
|
||
document.getElementById('intro-title').textContent = MASTER.title;
|
||
var _introStory = (MASTER.player?'Salut, '+MASTER.player+'! ':'')+MASTER.story;
|
||
document.getElementById('intro-story').textContent = _introStory;
|
||
document.getElementById('intro-promise').textContent = N+' camere \\u00b7 ${nStyles} stiluri \\u00b7 1 cuvânt magic';
|
||
document.getElementById('btn-start').onclick = function(){
|
||
unlockAudio(); /* gest direct pe părinte (handlerul global prinde și el, dar e idempotent) */
|
||
clearProgress(); owResetPlayer(); showOverworld(0);
|
||
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 ? '🔊' : '🔇'; /* 🔊 / 🔇 */
|
||
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 ? '🎵' : '🔇'; /* 🎵 / 🔇 */
|
||
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'); });
|
||
/* repornim pe hartă, la ușa camerei next */
|
||
var resumeIdx = saved.idx + 1;
|
||
/* marchează ușile deja rezolvate pe hartă (resume) */
|
||
for(var di=0; di<=saved.idx; di++){ roomDone[di] = true; setDot(di,'done'); }
|
||
if(resumeIdx >= N){
|
||
/* ultima cameră deja terminată — mergi direct la final */
|
||
showFinale(); return;
|
||
}
|
||
owResetPlayer(); showOverworld(resumeIdx);
|
||
startTimer(); /* resume → reia ceasul de la deadline-ul absolut salvat */
|
||
startMusic(); /* resume → reia muzica (T10) */
|
||
})();
|
||
<\/script>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
/* ---------- start ---------- */
|
||
|
||
renderGlobals();
|
||
renderPuzzles();
|
||
refreshPreview();
|
||
</script>
|
||
</body>
|
||
</html>
|