Campanie multi-stil — PR1 (T1-T8 + TD1-TD6)

Adauga al 6-lea stil de joc: campanie multi-stil care leaga puzzle-urile
in camere de stiluri diferite (clasic/terminal/arcade/chat/point in rotatie),
conectate prin coridoare cu usa, litera si stele.

Contract de montare (verificat la gate T1):
- gameCampaign: un <iframe srcdoc> per camera; camerele cheama parent.*
  pe un nivel (merge si pe file://); template per stil cu sentinel __CFG__
  injectat prin replace-functie (D1) + json.replace(/</g,'<') (D6)
- roomReady/roomError + timeout 4s -> skip cu 0 stele + cod eroare;
  idx detinut de parinte, accepta nextRoom doar de la contentWindow activ (D5)
- parent.beep in mod campanie (un singur AudioContext, D2)
- resume prin safeStore try/catch (D3) + cheie djb2 peste CFG embedat (D11)

Builder:
- selector de stil per puzzle ("Auto (stil)") + optiunea Campanie multi-stil
- normalizePuzzle() la load + import (sursa unica pt forma puzzle, D8)
- blocare export+preview la 0 puzzle-uri; persist() guarded (D12)
- letter normalizat [A-Za-z0-9] + esc la SVG point (D13)

Design (DESIGN.md): tokens --c-*, intro poster, coridor "usa ca erou",
chrome unica sursa de progres, 5 usi CSS/SVG (normal/stuck/crescendo),
mod camera per motor, buget vertical mobil, baseline a11y.

Tooling: tests/smoke.mjs (Playwright, zero-dependente prin npx), TODOS.md,
sectiune ## Testing in CLAUDE.md. Demo-uri regenerate + exemplu-campanie.html.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-13 08:34:57 +00:00
parent a464f642c0
commit a4b0ff4154
13 changed files with 2454 additions and 23 deletions

342
DESIGN.md Normal file
View File

@@ -0,0 +1,342 @@
# DESIGN.md — Escape Room Builder: Campania Multi-Stil
Contract de design pentru PR1 (Etapa 1 + Tooling). Aprobat la design review 2026-06-12 (scor 6/10 → 9/10).
Integratorul consultă acest fișier ca sursă unică de adevăr pentru toate deciziile vizuale și de interacțiune.
---
## 1. Tokens de design
Orchestratorul declară o dată în `:root` al `gameCampaign`. **Toate suprafețele de campanie consumă DOAR aceste variabile** — nu hardcodezi culori în coridor sau diplomă.
```css
:root {
--c-bg: #0d0620; /* fundal global campanie */
--c-surface: #221440; /* carduri, panouri */
--c-accent: <cfg.color>; /* accentul creatorului — suprascris la runtime */
--c-gold: #fbbf24; /* stele, crescendo, reward */
--c-line: rgba(255,255,255,.18);/* borduri subtile */
--c-ink: #fff; /* text principal */
}
```
**Note intenționate (nu se „repară"):**
- Violetul default (`#6d28d9`) și `system-ui` sunt deliberate — accentul aparține creatorului via `cfg.color`; webfonturile sunt imposibile pe `file://`.
- `@media print` suprascrie tokens pentru diplomă (fundal alb, borduri `--c-accent`).
---
## 2. Intro — Scenă-poster (§Design pct. 1)
**Structură (de sus în jos):**
1. Titlul jocului — cel mai mare element (`font-size: clamp(28px,6vw,48px)`, `font-weight: 900`)
2. „Salut, {nume}!" + povestea — secundar, `max-width: 60ch`, `color: rgba(255,255,255,.8)`
3. Promisiunea: „{N} camere · {S} stiluri · 1 cuvânt magic" — `font-size: 13px`, `color: rgba(255,255,255,.5)`
4. UN singur buton „Începe aventura" — full-width, `background: var(--c-accent)`
**Reguli:**
- Intro necronometrat (timer-ul din E2 pornește la click „Începe aventura").
- Fundalul folosește `--c-bg`; limbajul cardului final existent (fundal închis, accent creator).
- Povestea întreagă se spune O singură dată, aici. Camerele individuale NU repetă povestea.
---
## 3. Coridor — Ierarhie vizuală (§Design pct. 2 + mockup B aprobat)
```
┌──────────────────────────────────────┐
│ CHROME (48px / 40px mobil) │ ← unica sursă de progres global
│ ● ● ● ● ● (puncte camere) 0:00 │
├──────────────────────────────────────┤
│ │
│ ┌─────────────┐ litera câștigată │ ← strip compact: recompensa camerei
│ │ ★★★ + L │ stele camerei │
│ └─────────────┘ │
│ │
│ ╔═══════════╗ │ ← ușa DOMINĂ ecranul (estetica stilului următor)
│ ║ UȘADOOR ║ │ min-height: 40% din viewport
│ ╚═══════════╝ │
│ │
│ ┌──────────────────────────────┐ │ ← buton full-width, activ IMEDIAT
│ │ Deschide ușa → │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────┘
```
**Reguli:**
- Strip-ul (litera + stele) = **doar recompensa camerei curente**.
- Ușa este elementul erou — nu pune text lung lângă ea.
- Butonul „Deschide ușa" este activ **imediat** la montarea coridorului. Copilul deține ritmul.
- Butonul se dezactivează după primul click (idempotență, §T4).
---
## 4. Progres — Un singur owner (§Design pct. 3)
**Bara chrome = UNICA sursă de progres global.**
```
● ● ● ◉ ○ (puncte: parcurse=--c-gold, curentă=--c-accent, viitoare=stinse)
aria-label="Camera 3 din 5"
```
- Motoarele individuale **nu randează** progres de campanie (nu arată numărul camerei sau câte mai sunt).
- Strip-ul coridorului arată **doar** recompensa camerei (litera + stele). Nu total.
- `aria-label` obligatoriu pe containerul cu puncte (§A11y pct. 13).
---
## 5. Tranziția = Ușa (§Design pct. 4)
**Flow la click „Deschide ușa":**
```
click buton
→ ușa primește .opening → animație scale+fade ~250ms (transform-origin: left center)
→ coridorul rămâne pe ecran până la roomReady
→ [>1.5s fără roomReady] → puncte de încărcare discrete PE ușă (nu modal separat)
→ [4s fără roomReady] → skip automat (→ §5 Skip in-fiction)
→ roomReady primit → fade-in camera ~200ms (zero flash)
```
**CSS animație deschidere (din `scratch/doors.html`):**
```css
@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; }
}
.door-STIL.opening {
animation: door-open .25s cubic-bezier(.4,0,1,1) forwards;
transform-origin: left center; perspective: 600px;
}
```
**`prefers-reduced-motion`:** `.opening { animation: none; opacity: 0; }` — stările finale apar direct.
---
## 6. Skip In-Fiction — Ușa Înțepenită (§Design pct. 5)
**Camera moartă = aceeași compoziție de coridor cu ușa înțepenită.**
Aceeași structură ca §3, dar:
- Ușa primește clasa `.stuck` (grayscale + brightness 0.6) + badge `.door-lock` (🔒)
- Titlu: **„Ușa asta e înțepenită!"** (nu „Eroare" sau text tehnic)
- Buton primar: **„Sari la camera următoare"**
- Cod tehnic (stil·idx) — monospace mic, `color: rgba(255,255,255,.3)` — jos, fotografiabil
```html
<!-- Structura stuck state -->
<div class="corridor-stuck">
<div class="door-STIL stuck">
<span class="door-lock">🔒</span>
</div>
<h2>Ușa asta e înțepenită!</h2>
<button class="btn-primary">Sari la camera următoare</button>
<code class="err-code">terminal·2</code> <!-- stil·idx -->
</div>
```
**`roomError` post-ready** → același ecran (același handler, aceeași UI).
**Pe final/diplomă** → camera sărită = ușă înțepenită + 0 stele + dală goală (fără literă).
---
## 7. Ritm + Crescendo (§Design pct. 6)
- **Buget total animație per coridor: < 1s** (paralel, nu serial)
- **Butonul activ IMEDIAT** copilul nu așteaptă animații
- **Ultimul coridor** = crescendo:
- Ușa finală mai mare (`.crescendo` class scale 1.2 + glow per stil)
- Text suplimentar: **Ultima cameră!"** (`font-weight: 700`, `color: var(--c-gold)`)
- Buton: Deschide ultima ușă" (nu just Deschide ușa")
---
## 8. Cele 5 Uși — Spec CSS/SVG (§Design pct. 7)
**Fișier de referință:** `scratch/doors.html` vizualizare live cu toate stările.
**3 stări per ușă:** `.normal` (implicit) / `.stuck` (grayscale+lacăt) / `.crescendo` (scale+glow).
### Door 1 — Classic
```
Ușă-card albă · colțuri rotunjite (border-radius: 10px) · „?" auriu centrat
Crescendo: chenar auriu (box-shadow: 0 0 0 3px var(--c-gold))
```
### Door 2 — Terminal
```
Dreptunghi negru · ramă verde fosforescent (border: 2px solid #39ff6e, glow 14px)
Cursor _ clipind (animation: dt-blink 1s step-end infinite)
Scanlines (::before, repeating-linear-gradient)
Stuck: border-color: #444, cursor animation: none
Crescendo: double-ring + glow intensificat
```
### Door 3 — Arcade
```
Pixel-art chunky: border: 4px solid #4ade80 + box-shadow: inset 0 0 0 4px #166534, 0 0 0 4px #0a1606
Mâner galben pătrat (::after, 10×10px, no border-radius)
image-rendering: pixelated
Crescendo: outer ring #4ade80 + glow verde
```
### Door 4 — Chat
```
Siluetă telefon: gradient albastru (#1d4ed8→#1e3a8a), border-radius: 12px
Screen dark cu bule de mesaj (NPC: #1e40af stânga, player: #3b82f6 dreapta)
Home bar (22px, rgba(255,255,255,.3))
Crescendo: box-shadow: 0 0 0 2px #3b82f6 + glow albastru
```
### Door 5 — Point
```
SVG: ușă de scenă din lemn (#7c4f2c), 2 panouri superioare (rgba(0,0,0,.22))
Clanță rotundă (#f3cf6d), lupă (cerc + linie diagonală, stroke: #f3cf6d, width 3)
Crescendo: filter drop-shadow(0 0 12px rgba(243,207,109,.6))
```
---
## 9. Final ≠ Diplomă (§Design pct. 8)
**Ecranul final celebrează DOAR.** Nu afișează diploma.
```
Evadare reușită!
★★★★★ (stele totale)
[D] [A] [N] [C] [E] ← cuvântul magic, literă-cu-literă, 0.18s delay
Mesajul creatorului
[Joacă din nou] [Vezi diploma →] ← buton secundar „Vezi diploma" (Etapa 2)
```
**Camera sărită** = dală goală cu 🔒 (fără literă, fără animație flip).
---
## 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.
- **Format:** A4 portret, fundal ALB, chenar dublu `var(--c-accent)`
- **Titlu:** DIPLOMĂ DE EVADARE" **singurul** element cu font serif (limbajul certificatelor)
- **Ierarhie:** Numele copilului = cel mai mare element pe pagină
- **Rând de stele per cameră** camerele sărite = 🔒
- **Cuvântul magic** în dăle, aceeași iconografie ca finalul
- **Footer:** dată + creat de {creator}" + marcaj discret timpul a expirat" (dacă e cazul)
- **`@media print`:** doar diploma, `margin: 20mm`, `print-color-adjust: exact` pe accente, `background: white`
---
## 11. Timer Calm (§Design pct. 10 — Etapa 2 / PR2)
> Implementare în T10/PR2.
- Pornește **exact** la click Începe aventura" (intro necronometrat)
- Afișat în chrome: `M:SS`, neutru (`color: var(--c-ink)`)
- **Sub 1 minut** devine auriu (`color: var(--c-gold)`) NU roșu pulsant (public copii)
- La **expirare** îngheață pe `0:00` + marcaj text discret; jocul curge nestingherit (zero penalizare, stelele rămân)
- **Deadline absolut** în `sessionStorage` resume-ul nu resetează ceasul
---
## 12. Buget Vertical Mobil (§Design pct. 11)
```
Chrome: 48px desktop / 40px la max-width: 600px (doar puncte + timer, fără etichete)
Camerele: height = calc(100dvh - <chrome-height>) (dvh pentru mobile address bar)
Arcade: max-height pe canvas → scalare automată să încapă în viewport
```
**Assert obligatoriu în `tests/smoke.mjs`:**
```js
// La viewport 320×568, niciun element nu depășește documentElement.scrollWidth
assert(document.documentElement.scrollWidth <= 320, 'overflow orizontal 320px')
```
**Reguli:**
- ZERO scroll orizontal la 320×568 (iPhone SE)
- Fără `overflow-x: auto` pe coridor sau cameră rezolvă problema, nu o ascunde
---
## 13. Mod Cameră — Regulă Funcțională (§Design pct. 12 — închide D14)
**În campanie, fiecare motor ascunde:** (a) titlul jocului, (b) progresul global propriu, (c) restart-ul propriu.
**Părintele deține** toate acestea (bara chrome). Motoarele **păstrează** HUD-ul de gameplay și identitatea stilului.
| Motor | Ascunde (în campanie) | Păstrează |
|----------|---------------------------------|----------------------------------|
| Classic | `h1` + bara de progres + contor | Întrebări, stele per puzzle |
| Terminal | Titlul mare (linia banner) | Antetul de 1 linie, comenzi |
| Arcade | `h1` | HUD chei/stele cameră, canvas |
| Chat | Subtitlul jocului | Header-ul personajului, bule |
| Point | `h1` | Hint-ul de scenă, obiectele SVG |
**Implementare:** când `CFG._campaign` există, motorul adaugă clasa `.campaign-mode` pe `body`.
CSS: `.campaign-mode h1, .campaign-mode .game-progress { display: none; }` (specific per motor).
---
## 14. A11y Baseline — Suprafețe Noi (§Design pct. 13)
Aplicabil pe toate suprafețele adăugate în PR1 (coridor, chrome, intro, skip, final).
**Nu este audit al motoarelor existente** acela e în `TODOS.md`.
| Criteriu | Specificație |
|----------|-------------|
| Ținte tap | Minim 44×44px (buton Deschide ușa", puncte chrome, buton skip) |
| Contrast text | `rgba(255,255,255,.7)` pe `--c-bg` 4.5:1 text secundar |
| Focus | Buton Deschide ușa" focusabil + activabil cu Enter |
| ARIA | `aria-label="Camera X din Y"` pe containerul punctelor de progres |
| Motion | `@media (prefers-reduced-motion: reduce)` dezactivează: confetti, flipin, animația ușii stările finale apar direct |
---
## 15. Tabel Stări per Suprafață
| Suprafață | Loading | Empty | Error | Success | Partial |
|-----------|---------|-------|-------|---------|---------|
| Coridor cameră | ușa .opening; puncte pe ușă >1.5s | — | ușă .stuck + skip (§6) | fade-in cameră 200ms | — |
| Builder (campanie) | — | 0 puzzle: mesaj ghidant, export blocat | stil invalid: avertisment + rotație | preview rulează | — |
| Resume | — | — | hash invalid → restart silențios de la intro | coridorul camerei curente | refresh mid-campanie → coridor |
| Final | — | — | camere sărite: 🔒 + dală goală | stele + cuvânt + confetti | cuvânt cu dăle-lacăt |
| Skip camera | — | — | roomError (oricând) → ecran stuck | — | 0 stele, fără literă, cod eroare |
---
## 16. Storyboard Emoțional
| Pas | Copilul | Simte | Susținut de |
|-----|---------|-------|-------------|
| Intro | vede titlul + promisiunea | curiozitate | §2 (poster) |
| Camera 1 | joacă primul stil | concentrare | §12 (mod cameră discret), chrome neutru |
| Coridor 1 | primește litera, vede ușa nouă | mândrie + anticipare | §3, §5, §8 |
| Camerele 2..N-1 | stil nou de fiecare dată | varietate, „ce urmează?" | rotație + ritual <1s 7) |
| Ultimul coridor | ușa mai mare, Ultima cameră!" | crescendo | §7 (crescendo) |
| Final | cuvântul magic se dezvăluie | triumf | §9 (final celebrare pură) |
| Diplomă (E2) | o printează cu părintele | mândrie fizică | §10 (A4 print-first) |
---
## 17. NOT in Scope (cu motiv)
| Funcție | Motiv excludere |
|---------|-----------------|
| Audit a11y motoare existente | Post-PR1, sub harness Playwright în `TODOS.md` |
| Timer implementare | PR2/Etapa 2 (T10) |
| Diplomă implementare | PR2/Etapa 2 (T10) |
| Auto-advance la coridor | Respins (6C): agenția copilului primează la momentul-recompensă |
| Overlay tehnic de skip | Respins (5B): eșecul rămâne în ficțiune (ușă înțepenită) |
| Diploma pe ecranul final | Respins (3B): one job per section |
---
## 18. Fișiere de Referință
| Artefact | Locație | Conținut |
|----------|---------|---------|
| Uși (cod) | `scratch/doors.html` | 5 uși CSS/SVG × 3 stări, animație deschidere, tokens |
| Mockup aprobat | `~/.gstack/projects/romfast-escape-builder/designs/coridor-campanie-20260612/` | wireframes.html, coridor-3-variante.png (varianta B), approved.json |
| Plan complet | `~/.claude/plans/home-claude-claude-plans-propune-alte-u-replicated-papert.md` | §Design, task-uri, review reports |