Diploma A4 print-first (§Design pct.9): certificat la final campanie

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>
This commit is contained in:
Claude Agent
2026-06-13 20:20:59 +00:00
parent d8cb515545
commit 023df382f0
7 changed files with 308 additions and 13 deletions

View File

@@ -20,9 +20,9 @@ sursa de adevăr tehnică pentru agenți.
python3 -m http.server 8000 python3 -m http.server 8000
# Teste (Playwright; fără package.json commitat — vezi tests/AGENTS.md): # Teste (Playwright; fără package.json commitat — vezi tests/AGENTS.md):
npx playwright test tests/smoke.mjs # suita completă: 30/30 npx playwright test tests/smoke.mjs # suita completă: 31/31
npx playwright test tests/smoke.mjs --grep @regresie # regresie: 16 npx playwright test tests/smoke.mjs --grep @regresie # regresie: 16
npx playwright test tests/smoke.mjs --grep @campanie # campanie E2E: 16 npx playwright test tests/smoke.mjs --grep @campanie # campanie E2E: 17
``` ```
## Durable Rules (repo-wide) ## Durable Rules (repo-wide)

View File

@@ -217,7 +217,10 @@ Mesajul creatorului
## 10. Diplomă — Certificat A4 Print-First (§Design pct. 9 — Etapa 2 / PR2) ## 10. Diplomă — Certificat A4 Print-First (§Design pct. 9 — Etapa 2 / PR2)
> Implementare în T10/PR2. Spec inclusă aici ca parte a contractului de design. > **LIVRAT** (2026-06-13). Overlay `#diploma`; buton „Vezi diploma →" pe finale + „Joacă din nou".
> `buildDiploma()` citește `roomStars`/`collected`/`skipped`/`MASTER`/`_timerExpired`. Câmp builder nou
> `creator` („Creat de"). `@media print` izolează `#diploma` (rest `visibility:hidden`). Test smoke
> „diploma" (nume/titlu/stele-per-cameră/cuvânt/creator/înapoi). Camere sărite = 🔒 (verificat vizual).
- **Format:** A4 portret, fundal ALB, chenar dublu `var(--c-accent)` - **Format:** A4 portret, fundal ALB, chenar dublu `var(--c-accent)`
- **Titlu:** DIPLOMĂ DE EVADARE" **singurul** element cu font serif (limbajul certificatelor) - **Titlu:** DIPLOMĂ DE EVADARE" **singurul** element cu font serif (limbajul certificatelor)

View File

@@ -18,7 +18,7 @@ Referință plan complet: `~/.gstack/projects/romfast-escape-builder/ceo-plans/2
- [x] **Audit a11y motoare** — LIVRAT (vezi §dedicată mai jos). Smoke 26/26. - [x] **Audit a11y motoare** — LIVRAT (vezi §dedicată mai jos). Smoke 26/26.
**PR2 livrat (2026-06-13):** audio camere `651025b`, voce `da93d84`, unificare `ab11089`, a11y (acest commit). **PR2 livrat (2026-06-13):** audio camere `651025b`, voce `da93d84`, unificare `ab11089`, a11y (acest commit).
Rămas din Etapa 2: Adventure Mode v0 (+ Diplomă §Design pct.9). (D7 + Timer Calm + Muzică T10 LIVRATE.) Rămas din Etapa 2: Adventure Mode v0. (D7 + Timer Calm + Muzică T10 + Diplomă LIVRATE — vezi §§ mai jos.)
### [x] Bomberman polish (feedback user 2026-06-13) — LIVRAT ### [x] Bomberman polish (feedback user 2026-06-13) — LIVRAT
Trei probleme raportate + o lipsă, toate în `gameArcade` (`escape-builder.html`): Trei probleme raportate + o lipsă, toate în `gameArcade` (`escape-builder.html`):
@@ -96,6 +96,16 @@ portează în `escape-builder.html` (un singur fișier, integrare secvențială)
## Post-PR1 (după ship-ul campaniei) ## Post-PR1 (după ship-ul campaniei)
### [x] Diplomă A4 print-first (§Design pct.9) — LIVRAT (2026-06-13)
Certificat A4 portret, fundal alb, chenar dublu accent, titlu serif „DIPLOMĂ DE EVADARE" (singurul serif),
numele copilului = cel mai mare element. Overlay `#diploma`; buton „Vezi diploma →" pe finale (+ „Joacă
din nou"). `buildDiploma()` randează: rând de stele per cameră (★★★/★★☆; camere sărite = 🔒 „sărită"),
cuvântul magic în dăle (aceeași iconografie ca finalul, lacăte pentru sărite), footer = dată +
„creat de {creator}" + marcaj auriu „timpul a expirat" (dacă `_timerExpired`). Câmp builder nou `creator`.
Per-cameră `roomStars[]` (persistat în resume). `@media print` izolează `#diploma` (rest `visibility:hidden`,
`margin:20mm`, `print-color-adjust:exact`). Verificat: smoke 31/31 (test nou „diploma") + screenshot
(`scratch/diploma.png`: A4, 🔒 cameră sărită, footer expirat). Rămas din Etapa 2: doar Adventure Mode v0.
### [x] Timer Calm (§Design pct.10 / T10) — LIVRAT (2026-06-13) ### [x] Timer Calm (§Design pct.10 / T10) — LIVRAT (2026-06-13)
Ceas M:SS în bara chrome a campaniei. Opt-in din builder (câmp „Timp limită (minute)", default 0 = fără; Ceas M:SS în bara chrome a campaniei. Opt-in din builder (câmp „Timp limită (minute)", default 0 = fără;
`cleanState` coerce la întreg 0..120). Pornește la „Începe aventura" (intro necronometrat); deadline `cleanState` coerce la întreg 0..120). Pornește la „Începe aventura" (intro necronometrat); deadline

View File

@@ -130,6 +130,8 @@
</div> </div>
<label>Pentru cine (optional, apare in mesaje)</label> <label>Pentru cine (optional, apare in mesaje)</label>
<input type="text" id="gPlayer" data-g="player" placeholder="ex: Paula"> <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> <label>Povestea de inceput</label>
<textarea id="gStory" data-g="story" rows="3"></textarea> <textarea id="gStory" data-g="story" rows="3"></textarea>
<label>Mesajul final (la castig)</label> <label>Mesajul final (la castig)</label>
@@ -178,6 +180,7 @@ const defaultState = () => ({
player: '', player: '',
color: '#6d28d9', color: '#6d28d9',
style: 'classic', style: 'classic',
creator: '',
charName: 'Alex', charName: 'Alex',
voice: false, voice: false,
music: false, music: false,
@@ -1793,6 +1796,52 @@ body {
} }
.btn-main:hover { filter: brightness(1.1); } .btn-main:hover { filter: brightness(1.1); }
.btn-main:disabled { opacity: .5; cursor: not-allowed; } .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 { .btn-skip {
font: inherit; font-size: 15px; font-weight: 700; font: inherit; font-size: 15px; font-weight: 700;
background: #4b5563; color: #fff; border: none; background: #4b5563; color: #fff; border: none;
@@ -1856,6 +1905,30 @@ body {
<p>Cuvântul magic:</p> <p>Cuvântul magic:</p>
<div id="fin-word"></div> <div id="fin-word"></div>
<p id="fin-msg"></p> <p id="fin-msg"></p>
<div class="fin-actions">
<button class="btn-main" id="btn-diploma">Vezi diploma &rarr;</button>
<button class="btn-sec" id="btn-replay">Joacă din nou</button>
</div>
</div>
<!-- Diplomă A4 print-first (§Design pct.9) — populată la „Vezi diploma" -->
<div id="diploma" class="overlay" aria-hidden="true">
<div class="dipl-sheet" role="document" aria-label="Diplomă de evadare">
<div class="dipl-frame">
<div class="dipl-title">DIPLOMĂ DE EVADARE</div>
<div class="dipl-sub">se acordă lui</div>
<div class="dipl-name" id="dipl-name"></div>
<div class="dipl-game" id="dipl-game"></div>
<div class="dipl-rooms" id="dipl-rooms"></div>
<div class="dipl-wordlbl">Cuvântul magic</div>
<div class="dipl-word" id="dipl-word"></div>
<div class="dipl-footer" id="dipl-footer"></div>
</div>
</div>
<div class="dipl-actions">
<button class="btn-main" id="dipl-print">Printează diploma</button>
<button class="btn-sec" id="dipl-back">&larr; Înapoi</button>
</div>
</div> </div>
</div> </div>
@@ -1882,6 +1955,7 @@ document.documentElement.style.setProperty('--accent', MASTER.color || '#6d28d9'
var N = MASTER.puzzles.length; var N = MASTER.puzzles.length;
var totalStars = 0; var totalStars = 0;
var collected = []; var collected = [];
var roomStars = []; /* stele per cameră — pentru diplomă (§Design pct.9) */
var skipped = {}; var skipped = {};
var activeIdx = -1; var activeIdx = -1;
@@ -1899,7 +1973,7 @@ var _RESUME_KEY = 'esc-camp-' + djb2(JSON.stringify(MASTER));
function safeGet(){ try{ return JSON.parse(sessionStorage.getItem(_RESUME_KEY)); }catch(e){ return null; } } 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 safeSet(v){ try{ sessionStorage.setItem(_RESUME_KEY, JSON.stringify(v)); }catch(e){ /* webview restrictat — continuă fără resume */ } }
function saveProgress(){ function saveProgress(){
safeSet({ idx: activeIdx, totalStars: totalStars, collected: collected.slice(), skipped: skipped }); 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){} } function clearProgress(){ try{ sessionStorage.removeItem(_RESUME_KEY); sessionStorage.removeItem(_DEADLINE_KEY); }catch(e){} }
@@ -1988,6 +2062,7 @@ var frameEl = document.getElementById('room-frame');
var introEl = document.getElementById('intro'); var introEl = document.getElementById('intro');
var skipEl = document.getElementById('skip-banner'); var skipEl = document.getElementById('skip-banner');
var finaleEl = document.getElementById('finale'); var finaleEl = document.getElementById('finale');
var diplomaEl = document.getElementById('diploma');
/* ----- Dots ----- */ /* ----- Dots ----- */
function buildDots(){ function buildDots(){
@@ -2110,6 +2185,7 @@ window.nextRoom = function(data){
clearTimeout(readyTimer); clearTimeout(readyTimer);
roomDone[idx] = true; roomDone[idx] = true;
totalStars += (data.stars || 0); 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(); var letter = String(data.letter || '').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
if(letter) collected.push(letter); if(letter) collected.push(letter);
setDot(idx,'done'); setDot(idx,'done');
@@ -2240,6 +2316,43 @@ function showFinale(){
voiceSay(document.getElementById('fin-msg').textContent); /* citește mesajul final (D10) */ 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 ----- */ /* ----- Confetti ----- */
function confetti(){ function confetti(){
var colors=[MASTER.color||'#6d28d9','#fbbf24','#34d399','#60a5fa','#f472b6']; var colors=[MASTER.color||'#6d28d9','#fbbf24','#34d399','#60a5fa','#f472b6'];
@@ -2255,7 +2368,7 @@ function confetti(){
var overworldEl = document.getElementById('overworld'); var overworldEl = document.getElementById('overworld');
function hideAll(){ function hideAll(){
voiceCancel(); /* fără replici fantomă la schimbarea scenei (D10) */ voiceCancel(); /* fără replici fantomă la schimbarea scenei (D10) */
[introEl,overworldEl,skipEl,finaleEl].forEach(function(el){ el.classList.remove('show'); }); [introEl,overworldEl,skipEl,finaleEl,diplomaEl].forEach(function(el){ el.classList.remove('show'); });
} }
/* ===== Overworld (S3 pas2 — hartă top-down care înlocuiește coridorul) ===== /* ===== Overworld (S3 pas2 — hartă top-down care înlocuiește coridorul) =====
@@ -2433,6 +2546,7 @@ buildDots();
/* restaurăm starea */ /* restaurăm starea */
totalStars = saved.totalStars || 0; totalStars = saved.totalStars || 0;
collected = saved.collected || []; collected = saved.collected || [];
roomStars = saved.roomStars || [];
skipped = saved.skipped || {}; skipped = saved.skipped || {};
Object.keys(skipped).forEach(function(k){ roomDone[+k] = true; setDot(+k,'done'); }); Object.keys(skipped).forEach(function(k){ roomDone[+k] = true; setDot(+k,'done'); });
/* repornim pe hartă, la ușa camerei next */ /* repornim pe hartă, la ușa camerei next */

View File

@@ -195,6 +195,52 @@ body {
} }
.btn-main:hover { filter: brightness(1.1); } .btn-main:hover { filter: brightness(1.1); }
.btn-main:disabled { opacity: .5; cursor: not-allowed; } .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 { .btn-skip {
font: inherit; font-size: 15px; font-weight: 700; font: inherit; font-size: 15px; font-weight: 700;
background: #4b5563; color: #fff; border: none; background: #4b5563; color: #fff; border: none;
@@ -258,6 +304,30 @@ body {
<p>Cuvântul magic:</p> <p>Cuvântul magic:</p>
<div id="fin-word"></div> <div id="fin-word"></div>
<p id="fin-msg"></p> <p id="fin-msg"></p>
<div class="fin-actions">
<button class="btn-main" id="btn-diploma">Vezi diploma &rarr;</button>
<button class="btn-sec" id="btn-replay">Joacă din nou</button>
</div>
</div>
<!-- Diplomă A4 print-first (§Design pct.9) — populată la „Vezi diploma" -->
<div id="diploma" class="overlay" aria-hidden="true">
<div class="dipl-sheet" role="document" aria-label="Diplomă de evadare">
<div class="dipl-frame">
<div class="dipl-title">DIPLOMĂ DE EVADARE</div>
<div class="dipl-sub">se acordă lui</div>
<div class="dipl-name" id="dipl-name"></div>
<div class="dipl-game" id="dipl-game"></div>
<div class="dipl-rooms" id="dipl-rooms"></div>
<div class="dipl-wordlbl">Cuvântul magic</div>
<div class="dipl-word" id="dipl-word"></div>
<div class="dipl-footer" id="dipl-footer"></div>
</div>
</div>
<div class="dipl-actions">
<button class="btn-main" id="dipl-print">Printează diploma</button>
<button class="btn-sec" id="dipl-back">&larr; Înapoi</button>
</div>
</div> </div>
</div> </div>
@@ -284,6 +354,7 @@ document.documentElement.style.setProperty('--accent', MASTER.color || '#6d28d9'
var N = MASTER.puzzles.length; var N = MASTER.puzzles.length;
var totalStars = 0; var totalStars = 0;
var collected = []; var collected = [];
var roomStars = []; /* stele per cameră — pentru diplomă (§Design pct.9) */
var skipped = {}; var skipped = {};
var activeIdx = -1; var activeIdx = -1;
@@ -301,7 +372,7 @@ var _RESUME_KEY = 'esc-camp-' + djb2(JSON.stringify(MASTER));
function safeGet(){ try{ return JSON.parse(sessionStorage.getItem(_RESUME_KEY)); }catch(e){ return null; } } 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 safeSet(v){ try{ sessionStorage.setItem(_RESUME_KEY, JSON.stringify(v)); }catch(e){ /* webview restrictat — continuă fără resume */ } }
function saveProgress(){ function saveProgress(){
safeSet({ idx: activeIdx, totalStars: totalStars, collected: collected.slice(), skipped: skipped }); 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){} } function clearProgress(){ try{ sessionStorage.removeItem(_RESUME_KEY); sessionStorage.removeItem(_DEADLINE_KEY); }catch(e){} }
@@ -390,6 +461,7 @@ var frameEl = document.getElementById('room-frame');
var introEl = document.getElementById('intro'); var introEl = document.getElementById('intro');
var skipEl = document.getElementById('skip-banner'); var skipEl = document.getElementById('skip-banner');
var finaleEl = document.getElementById('finale'); var finaleEl = document.getElementById('finale');
var diplomaEl = document.getElementById('diploma');
/* ----- Dots ----- */ /* ----- Dots ----- */
function buildDots(){ function buildDots(){
@@ -512,6 +584,7 @@ window.nextRoom = function(data){
clearTimeout(readyTimer); clearTimeout(readyTimer);
roomDone[idx] = true; roomDone[idx] = true;
totalStars += (data.stars || 0); 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(); var letter = String(data.letter || '').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
if(letter) collected.push(letter); if(letter) collected.push(letter);
setDot(idx,'done'); setDot(idx,'done');
@@ -642,6 +715,43 @@ function showFinale(){
voiceSay(document.getElementById('fin-msg').textContent); /* citește mesajul final (D10) */ 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 ----- */ /* ----- Confetti ----- */
function confetti(){ function confetti(){
var colors=[MASTER.color||'#6d28d9','#fbbf24','#34d399','#60a5fa','#f472b6']; var colors=[MASTER.color||'#6d28d9','#fbbf24','#34d399','#60a5fa','#f472b6'];
@@ -657,7 +767,7 @@ function confetti(){
var overworldEl = document.getElementById('overworld'); var overworldEl = document.getElementById('overworld');
function hideAll(){ function hideAll(){
voiceCancel(); /* fără replici fantomă la schimbarea scenei (D10) */ voiceCancel(); /* fără replici fantomă la schimbarea scenei (D10) */
[introEl,overworldEl,skipEl,finaleEl].forEach(function(el){ el.classList.remove('show'); }); [introEl,overworldEl,skipEl,finaleEl,diplomaEl].forEach(function(el){ el.classList.remove('show'); });
} }
/* ===== Overworld (S3 pas2 — hartă top-down care înlocuiește coridorul) ===== /* ===== Overworld (S3 pas2 — hartă top-down care înlocuiește coridorul) =====
@@ -835,6 +945,7 @@ buildDots();
/* restaurăm starea */ /* restaurăm starea */
totalStars = saved.totalStars || 0; totalStars = saved.totalStars || 0;
collected = saved.collected || []; collected = saved.collected || [];
roomStars = saved.roomStars || [];
skipped = saved.skipped || {}; skipped = saved.skipped || {};
Object.keys(skipped).forEach(function(k){ roomDone[+k] = true; setDot(+k,'done'); }); Object.keys(skipped).forEach(function(k){ roomDone[+k] = true; setDot(+k,'done'); });
/* repornim pe hartă, la ușa camerei next */ /* repornim pe hartă, la ușa camerei next */

View File

@@ -5,7 +5,7 @@ Smoke + regresie + campanie E2E pentru jocurile generate. Verifică faptic: fiec
până la ecranul final, fără erori de consolă. până la ecranul final, fără erori de consolă.
## Ownership ## Ownership
- `tests/smoke.mjs` — unicul fișier de teste (~30 teste). - `tests/smoke.mjs` — unicul fișier de teste (~31 teste).
- `playwright.config.mjs` (la root, **gitignored**) — config dev. - `playwright.config.mjs` (la root, **gitignored**) — config dev.
## Local Contracts ## Local Contracts
@@ -18,9 +18,9 @@ până la ecranul final, fără erori de consolă.
fiecare test asertează `errors.length === 0` la final. fiecare test asertează `errors.length === 0` la final.
- **Tag-uri:** `@regresie` (16 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML + - **Tag-uri:** `@regresie` (16 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML +
stil top-level invalid la import + bomberman gameplay + bomberman rază/powerup-uri) și `@campanie` stil top-level invalid la import + bomberman gameplay + bomberman rază/powerup-uri) și `@campanie`
(16 — intro→hartă→camere→final, resume, cameră moartă, idempotență ușă, `$`/`$&`, beep, mobil, (17 — intro→hartă→camere→final, resume, cameră moartă, idempotență ușă, `$`/`$&`, beep, mobil,
audio S1, voce/narațiune D10, a11y tap/aria/reduced-motion, navigare overworld, timer calm T10, muzica ambient T10). audio S1, voce/narațiune D10, a11y tap/aria/reduced-motion, navigare overworld, timer calm T10, muzica ambient T10, diploma A4).
- **Status țintă: 30/30 PASS.** - **Status țintă: 31/31 PASS.**
## Work Guidance ## Work Guidance
- După modificări la motoare (`escape-builder.html`): rulează suita completă; extinde `@regresie` dacă - După modificări la motoare (`escape-builder.html`): rulează suita completă; extinde `@regresie` dacă
@@ -29,7 +29,7 @@ până la ecranul final, fără erori de consolă.
## Verification ## Verification
```bash ```bash
npx playwright test tests/smoke.mjs # 30/30 npx playwright test tests/smoke.mjs # 31/31
npx playwright test tests/smoke.mjs --grep @regresie npx playwright test tests/smoke.mjs --grep @regresie
npx playwright test tests/smoke.mjs --grep @campanie npx playwright test tests/smoke.mjs --grep @campanie
``` ```

View File

@@ -791,6 +791,63 @@ test.describe('Campanie E2E @campanie', () => {
expect(errors, errors.join('\n')).toHaveLength(0); expect(errors, errors.join('\n')).toHaveLength(0);
}); });
test('diploma — „Vezi diploma" arata certificat A4 cu nume/stele-per-camera/cuvant/creator, inapoi (§Design pct.9)',
async ({ page }) => {
test.setTimeout(120000);
const errors = trackErrors(page);
const cfg = campaignCfg(3, 'classic');
cfg.player = 'Maria';
cfg.creator = 'Doamna Ana';
const tmpPath = await writeCampaignHtml(page, cfg, 'diploma');
const gp = await page.context().newPage();
const gameErrors = trackErrors(gp);
try {
await gp.goto('file://' + tmpPath);
await gp.locator('#btn-start').click();
for (let i = 0; i < 3; i++) {
await enterRoom(gp, i);
await solveRoom(gp, 'classic', 'r' + (i + 1));
}
await gp.waitForFunction(() => document.getElementById('finale')?.classList.contains('show'),
null, { timeout: 10000 });
// Buton „Vezi diploma" → diploma vizibila, finale ascunsa
await gp.locator('#btn-diploma').click();
await expect(gp.locator('#diploma')).toBeVisible();
await expect(gp.locator('#finale')).not.toBeVisible();
// Numele copilului (cel mai mare element) + titlul jocului
await expect(gp.locator('#dipl-name')).toHaveText('Maria');
await expect(gp.locator('#dipl-game')).toContainText('Test Campanie');
// Rand de stele per camera: 3 randuri, fiecare cu ★ (rezolvate, nu sarite)
await expect(gp.locator('#dipl-rooms .dipl-room')).toHaveCount(3);
const firstRoom = await gp.locator('#dipl-rooms .dipl-room .rstars').first().innerText();
expect(firstRoom).toMatch(/[★☆]{3}/);
// Cuvantul magic in dale (3 litere colectate, A B C)
await expect(gp.locator('#dipl-word span')).toHaveCount(3);
// Footer: data + „creat de Doamna Ana"
await expect(gp.locator('#dipl-footer')).toContainText('creat de Doamna Ana');
// Butonul de print exista (nu il apasam — window.print blocheaza headless)
await expect(gp.locator('#dipl-print')).toBeVisible();
// Inapoi → finale din nou
await gp.locator('#dipl-back').click();
await expect(gp.locator('#finale')).toBeVisible();
await expect(gp.locator('#diploma')).not.toBeVisible();
} finally {
await gp.close();
try { unlinkSync(tmpPath); } catch (_) {}
}
expect(gameErrors, 'Game errors:\n' + gameErrors.join('\n')).toHaveLength(0);
expect(errors, errors.join('\n')).toHaveLength(0);
});
// ───────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────
// Test 3: Camera moartă — timeout 4s → skip-banner + cod eroare // Test 3: Camera moartă — timeout 4s → skip-banner + cod eroare
// ───────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────