Files
escape-builder/exemplu-point.html
Claude Agent ab11089097 PR2: unifica contractul _campaign la final in libJS.campaignDone()
Payload-ul parent.nextRoom({idx,stars,letter}) era scris identic in 3 locuri
(terminal finale(), SNIP.finalJs showFinal(), classic next()). Acum traieste o
singura data in libJS.campaignDone(), langa roomReady/beep/onerror.

Decizie: NU am pus terminalul pe showFinal() din SNIP (cum sugera formularea
initiala) — showFinal randeaza modal #fOverlay, dar terminalul are finale CRT
stilizat (ASCII 'EVADARE REUSITA' + RESTART), on-theme intentionat. Fortarea
modalului ar fi regresie vizuala pe terminalul standalone. Am unificat doar
ramura _campaign (payload identic), prezentarea standalone neatinsa.

- terminal finale(): say([...], 'ok', campaignDone)
- SNIP.finalJs showFinal(): if(_campaign){ campaignDone(); return; }
- arcade/chat/point primesc campaignDone via showFinal automat.
- classic ramane bespoke (nu foloseste libJS) — pliere = D7 (documentat in TODOS).

Suita 25/25 (terminal standalone + camere terminal in campanie E2E). Demo-uri
libJS regenerate; exemplu-clasic.html neatins.

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

222 lines
19 KiB
HTML

<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Comoara ascunsa</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)); } }
.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); } }
#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; }
#fOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.88); z-index: 30; align-items: center; justify-content: center; padding: 16px; }
#fOverlay .fcard { width: 100%; max-width: 480px; text-align: center; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 18px; padding: 28px; color: #fff; font-family: system-ui, sans-serif; }
#fOverlay h1 { margin: 0 0 8px; font-size: 26px; }
#fOverlay .fstars { font-size: 26px; letter-spacing: 4px; color: #fbbf24; margin: 6px 0; }
#fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; }
#fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; }
@keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
#fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; }
#fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }
</style>
</head>
<body>
<h1>Comoara ascunsa</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>
<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>
<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>
<script>
var CFG = {"title":"Comoara ascunsa","player":"","color":"#6d28d9","charName":"Alex","story":"O comoara a fost ascunsa, iar singurul drum spre ea trece prin cateva incercari.","finalMessage":"Felicitari! Ai gasit comoara!","puzzles":[{"title":"Incalzirea","type":"free","question":"Cat fac 7 x 8?","answer":"56","tfAnswer":"Adevarat","choices":"","hint":"Tabla inmultirii cu 7.","letter":"D","style":""},{"title":"Adevarat sau fals","type":"tf","question":"Romania are iesire la Marea Neagra.","answer":"","tfAnswer":"Adevarat","choices":"","hint":"","letter":"A","style":""},{"title":"Alege raspunsul","type":"choice","question":"Care este capitala Frantei?","answer":"","tfAnswer":"Adevarat","choices":"*Paris\nLyon\nMarsilia","hint":"Turnul Eiffel.","letter":"R","style":""}],"style":"point"};
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);
}
var N = CFG.puzzles.length, solvedFlags = [], solvedCount = 0;
var POOL = [
{ name: 'Ceasul', x: 308, y: 62, svg: '<circle cx="32" cy="32" r="30" fill="#caa856"/><circle cx="32" cy="32" r="24" fill="#f4ecd8"/><line x1="32" y1="32" x2="32" y2="16" stroke="#222" stroke-width="3"/><line x1="32" y1="32" x2="43" y2="36" stroke="#222" stroke-width="3"/><circle cx="32" cy="32" r="2.5" fill="#222"/>' },
{ name: 'Tabloul', x: 108, y: 84, svg: '<rect width="84" height="64" fill="#8a5a2b"/><rect x="6" y="6" width="72" height="52" fill="#b7d3e8"/><polygon points="10,52 34,24 50,44 60,32 74,52" fill="#4f7a4f"/><circle cx="62" cy="16" r="6" fill="#f7d774"/>' },
{ name: 'Sertarul biroului', x: 64, y: 300, svg: '<rect width="150" height="14" fill="#7a4a22"/><rect x="6" y="14" width="138" height="46" fill="#925a2c"/><rect x="20" y="22" width="110" height="28" fill="#7a4a22"/><circle cx="75" cy="36" r="4" fill="#f3cf6d"/><rect x="10" y="60" width="10" height="34" fill="#7a4a22"/><rect x="130" y="60" width="10" height="34" fill="#7a4a22"/>' },
{ name: 'Dulapul', x: 232, y: 228, svg: '<rect width="92" height="142" fill="#8a5a2b"/><rect x="5" y="5" width="38" height="132" fill="#a06a35"/><rect x="49" y="5" width="38" height="132" fill="#a06a35"/><circle cx="38" cy="72" r="3.5" fill="#f3cf6d"/><circle cx="54" cy="72" r="3.5" fill="#f3cf6d"/>' },
{ name: 'Fereastra', x: 424, y: 62, svg: '<rect width="96" height="120" fill="#6b7f9e"/><rect x="6" y="6" width="84" height="108" fill="#101d3a"/><line x1="48" y1="6" x2="48" y2="114" stroke="#6b7f9e" stroke-width="5"/><line x1="6" y1="60" x2="90" y2="60" stroke="#6b7f9e" stroke-width="5"/><circle cx="68" cy="32" r="11" fill="#f4f1de"/>' },
{ name: 'Raftul cu carti', x: 558, y: 78, svg: '<rect y="34" width="120" height="8" fill="#7a4a22"/><rect x="6" y="6" width="13" height="28" fill="#b54a4a"/><rect x="21" y="2" width="13" height="32" fill="#4a7ab5"/><rect x="36" y="8" width="13" height="26" fill="#54a05e"/><rect x="51" y="4" width="13" height="30" fill="#c2a23e"/><rect x="66" y="9" width="13" height="25" fill="#9a5ab5"/><rect x="81" y="5" width="13" height="29" fill="#b5764a"/><rect x="96" y="8" width="13" height="26" fill="#5aa0b5"/>' },
{ name: 'Cutia', x: 404, y: 330, svg: '<rect width="66" height="50" fill="#925a2c"/><line x1="0" y1="0" x2="66" y2="50" stroke="#7a4a22" stroke-width="5"/><line x1="66" y1="0" x2="0" y2="50" stroke="#7a4a22" stroke-width="5"/><rect width="66" height="50" fill="none" stroke="#7a4a22" stroke-width="6"/>' },
{ name: 'Lampa', x: 516, y: 252, svg: '<polygon points="14,0 50,0 60,34 4,34" fill="#d9a23e"/><rect x="29" y="34" width="6" height="78" fill="#555"/><rect x="12" y="112" width="40" height="8" rx="3" fill="#555"/><circle cx="32" cy="17" r="9" fill="#ffe9a8" opacity=".8"/>' },
{ name: 'Seiful', x: 596, y: 300, svg: '<rect width="74" height="74" rx="6" fill="#5b6470"/><rect x="7" y="7" width="60" height="60" rx="4" fill="#434b55"/><circle cx="37" cy="37" r="14" fill="#5b6470"/><circle cx="37" cy="37" r="9" fill="#2c3138"/><line x1="37" y1="28" x2="37" y2="37" stroke="#d4d4d4" stroke-width="2.5"/>' },
{ name: 'Covorul', x: 250, y: 432, svg: '<ellipse cx="110" cy="26" rx="110" ry="26" fill="#7a3b56"/><ellipse cx="110" cy="26" rx="80" ry="17" fill="#94506c"/><ellipse cx="110" cy="26" rx="46" ry="9" fill="#7a3b56"/>' }
];
function crate(i){ return { name: 'Lada ' + (i + 1), x: 50 + ((i - 10) % 8) * 86, y: 408, svg: POOL[6].svg }; }
var base = '<rect width="800" height="380" fill="#3b2a63"/><rect y="380" width="800" height="120" fill="#241a3f"/><rect y="372" width="800" height="8" fill="#1c1336"/>'
+ '<g id="door"><rect x="694" y="148" width="86" height="232" fill="#6b4226"/><rect x="702" y="156" width="70" height="216" fill="#8a5a2b"/><circle cx="712" cy="266" r="5" fill="#f3cf6d"/><g id="lock"><rect x="730" y="250" width="26" height="22" rx="4" fill="#caa856"/><path d="M735 250 v-7 a8 8 0 0 1 16 0 v7" fill="none" stroke="#caa856" stroke-width="4"/></g></g>';
var objs = '';
for (var i = 0; i < N; i++) {
var o = POOL[i] || crate(i);
objs += '<g class="hot" data-i="' + i + '" transform="translate(' + o.x + ',' + o.y + ')">' + o.svg + '<title>' + o.name + '</title></g>';
}
el('scene').innerHTML = base + objs;
el('scene').addEventListener('click', function(e){
var t = e.target;
var g = t.closest ? t.closest('g.hot') : null;
if (g) {
if (!g.classList.contains('done')) openPuzzle(+g.getAttribute('data-i'), onSolved);
return;
}
var door = t.closest ? t.closest('#door') : null;
if (door) {
if (solvedCount >= N) { showFinal(); }
else { beep(false); el('note').textContent = 'Usa e incuiata! Mai ai ' + (N - solvedCount) + ' obiecte de cercetat.'; }
}
});
function onSolved(i){
solvedFlags[i] = true; solvedCount++;
var g = document.querySelector('g.hot[data-i="' + i + '"]');
g.classList.add('done');
var L = (CFG.puzzles[i].letter || '').trim();
g.innerHTML += '<circle cx="0" cy="0" r="13" fill="#16a34a"/><text x="0" y="5" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">' + (L ? L.toUpperCase() : '\u2713') + '</text>';
updateHud();
if (solvedCount >= N) {
el('door').classList.add('open');
el('note').textContent = 'Toate obiectele rezolvate! Da click pe usa ca sa evadezi.';
beep(true);
}
}
function updateHud(){
el('hudStep').textContent = 'Obiecte: ' + solvedCount + '/' + N;
el('hudStars').textContent = totalStars + ' \u2605';
var hb = el('hudLetters'); hb.innerHTML = '';
for (var j = 0; j < N; j++) {
var L = (CFG.puzzles[j].letter || '').trim();
if (!L) continue;
var s = document.createElement('span');
if (solvedFlags[j]) { s.textContent = L.toUpperCase(); s.className = 'won'; }
else s.textContent = '?';
hb.appendChild(s);
}
}
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');
}
}
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(); };
updateHud();
roomReady();
</script>
</body>
</html>