Ceas M:SS in bara chrome a campaniei. Opt-in din builder (camp "Timp limita (minute)", default 0 = fara; cleanState coerce 0..120). - porneste exact la "Incepe aventura" (intro necronometrat) - deadline ABSOLUT in sessionStorage -> resume nu reseteaza ceasul - sub 1 min -> auriu (.low); expirare -> ingheata 0:00 + marcaj discret (.expired), jocul curge nestingherit (zero penalizare, stelele raman) - fara rosu pulsant (public copii) -> reduced-motion safe by default - exemplu-campanie.html regenerat (ramane fara timer - opt-in, ca vocea) Fundatie pentru muzica T10 (accelerare sub 1 min) + footer diploma. Test nou (smoke 29/29): format M:SS, prag auriu, freeze la expirare, jocul continua dupa expirare, resume pastreaza ceasul. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
346 lines
15 KiB
Markdown
346 lines
15 KiB
Markdown
# 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)
|
||
|
||
> **LIVRAT** (2026-06-13). Opt-in din builder (câmp „Timp limită (minute)", default 0 = fără).
|
||
> Implementare: `#chrome-timer` în bara chrome; `startTimer/tickTimer/stopTimer`; deadline absolut
|
||
> în `sessionStorage` (`_DEADLINE_KEY`). Sub 1 min → `.low` (auriu); expirat → `.expired` (auriu, opac).
|
||
> Test smoke „timer calm" (format, gold, freeze, resume păstrează ceasul).
|
||
|
||
- 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 |
|