Compare commits
18 Commits
a4b0ff4154
...
ba949f43b0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba949f43b0 | ||
|
|
cb7eaffdf7 | ||
|
|
a30441eb03 | ||
|
|
ab11089097 | ||
|
|
da93d8498c | ||
|
|
651025bd28 | ||
|
|
27fc0ca901 | ||
|
|
463e3cc9bd | ||
|
|
05f4b4fe5a | ||
|
|
cead5c5156 | ||
|
|
4454df9c3b | ||
|
|
309103fb59 | ||
|
|
d67f6ddc15 | ||
|
|
5f78eef289 | ||
|
|
a9f30650d5 | ||
|
|
52f97af533 | ||
|
|
b935a21b41 | ||
|
|
a42c960b46 |
67
AGENTS.md
Normal file
67
AGENTS.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# AGENTS.md — Escape Room Builder (root)
|
||||
|
||||
Generator de jocuri escape room: un singur fișier HTML, fără backend/build/dependențe.
|
||||
Același set de puzzle-uri se exportă în 6 motoare de joc. Acest arbore `AGENTS.md` e
|
||||
sursa de adevăr tehnică pentru agenți.
|
||||
|
||||
## Protocol DOX
|
||||
|
||||
- **Citire** — înainte de a edita un path, cobori din rădăcină citind fiecare `AGENTS.md`
|
||||
întâlnit; dacă un părinte indexează un copil al cărui scope conține path-ul, citești copilul.
|
||||
- **Editare** — `AGENTS.md`-ul cel mai apropiat = contract local; părinții = reguli globale.
|
||||
Un copil NU poate slăbi o constrângere a unui părinte.
|
||||
- **Update** — după orice schimbare semnificativă, actualizezi `AGENTS.md`-ul care deține zona
|
||||
+ indexul părintelui; ștergi textul învechit.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Rulare/verificare — deschide direct în browser (merge de pe file://) sau servește local:
|
||||
python3 -m http.server 8000
|
||||
|
||||
# Teste (Playwright; fără package.json commitat — vezi tests/AGENTS.md):
|
||||
npx playwright test tests/smoke.mjs # suita completă: 27/27
|
||||
npx playwright test tests/smoke.mjs --grep @regresie # regresie: 15
|
||||
npx playwright test tests/smoke.mjs --grep @campanie # campanie E2E: 12
|
||||
```
|
||||
|
||||
## Durable Rules (repo-wide)
|
||||
|
||||
- **Zero dependențe.** Produsul (fișierele `*.html`) e vanilla HTML/CSS/JS, merge offline de pe
|
||||
`file://`. `node_modules/`, `package.json`, `playwright.config.mjs`, `scratch/`, `test-results/`
|
||||
sunt **gitignored** — doar dev tooling, nu fac parte din produs.
|
||||
- **Un singur fișier.** Toată aplicația trăiește în `escape-builder.html` (~1960 linii), pe secțiuni
|
||||
comentate: `stare` · `editor` · `preview` · `template-urile jocului exportat`.
|
||||
- **Dispatch.** `gameHTML(cfg)` rutează pe `cfg.style` către 6 motoare:
|
||||
`gameClassic · gameTerminal · gameArcade · gameChat · gamePoint · gameCampaign`. Fiecare returnează
|
||||
un string HTML complet, standalone.
|
||||
- **Cod partajat = blast radius global.** `libJS(cfg)` (`CFG`, `norm`, `checkAnswer`, `starsFor`,
|
||||
`finalWord`, `beep`, `confetti`) și `SNIP.*` (`baseCss`, modal, ecran final) sunt injectate în
|
||||
TOATE motoarele. O schimbare aici → verifică fiecare stil în preview înainte de commit.
|
||||
- **Backslash dublu.** Motoarele sunt template literals mari: backslash-urile din codul GENERAT se
|
||||
dublează (`\\u0300`, `\\n`). Codul generat folosește `var`/`function` clasic — intenționat.
|
||||
- **Sentinel `__CFG__`.** Templatele per stil emit `__CFG__` (la `cfg === '__TEMPLATE__'`) și se
|
||||
injectează prin **replace-FUNCȚIE**, nu string — protejează `$`/`$&` din config (D1).
|
||||
- **Demo-urile sunt generate.** `exemplu-*.html` = jocuri exportate din builder, unul per stil.
|
||||
**NU le edita manual** — după modificări la motoare, regenerează prin export. `index.html` = doar
|
||||
landing care leagă builder-ul + demo-urile.
|
||||
- **Stare.** Obiectul `state` (titlu, poveste, culoare, `style`, `puzzles`) se persistă în
|
||||
`localStorage` sub cheia `escape-builder-v1`; export/import ca JSON. Editorul scrie via `data-g` →
|
||||
`onChange()` → persist + `refreshPreview()` (debounce 400ms) care setează `iframe.srcdoc`.
|
||||
- **Design = `DESIGN.md`.** Sursă unică de adevăr vizual pentru campanie (tokens `:root`, intro-poster,
|
||||
coridor, diplomă). Note „nu se repară" (violet default, `system-ui`, lipsa webfonturilor pe `file://`)
|
||||
sunt deliberate — consultă fișierul înainte de schimbări vizuale.
|
||||
|
||||
## Documente conexe (conținut, nu contracte)
|
||||
|
||||
- `DESIGN.md` — contract de design campanie (vizual/interacțiune).
|
||||
- `TODOS.md` — **board de progres durabil** + backlog. Secțiunea „▶ BOARD ACTIV" sus = sursa de
|
||||
adevăr pentru ce e în lucru; harness task list-ul se resetează între sesiuni, ăsta nu. La
|
||||
START de sesiune citește board-ul; mută `[ ]→[~]→[x]→[!]` pe măsură ce avansezi și commit.
|
||||
- `HANDOFF.md` — handoff de sesiune (efemer; direcția curentă de lucru).
|
||||
- `README.md` — descriere pentru utilizatorul final.
|
||||
|
||||
## Child DOX Index
|
||||
|
||||
- `tests/` → [tests/AGENTS.md](tests/AGENTS.md) — harness Playwright (smoke/regresie/campanie E2E).
|
||||
- `scratch/` → scratch/AGENTS.md — zonă de prototipuri & QA (gitignored, efemer).
|
||||
72
CLAUDE.md
72
CLAUDE.md
@@ -1,65 +1,23 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
Ghid pentru Claude Code în acest repo.
|
||||
|
||||
## Ce este
|
||||
## Sursa de adevăr: arborele AGENTS.md
|
||||
|
||||
Generator de jocuri escape room: un singur fișier HTML (`escape-builder.html`), fără backend, fără build, fără dependențe. Același set de puzzle-uri se exportă în 5 stiluri de joc diferite (clasic/quiz, terminal retro, arcade pixel, story chat, point-and-click).
|
||||
Adevărul tehnic (arhitectură, reguli de editare, testare) trăiește în arborele `AGENTS.md`, nu aici.
|
||||
Înainte să editezi un path, citește lanțul DOX din rădăcină:
|
||||
|
||||
## Dezvoltare
|
||||
- [`AGENTS.md`](AGENTS.md) — root: ce e produsul, Quick Reference, Durable Rules (zero-dep, un singur
|
||||
fișier, backslash dublu, sentinel `__CFG__`, demo-uri generate, `libJS`/`SNIP` partajate, dispatch
|
||||
pe 6 motoare).
|
||||
- [`tests/AGENTS.md`](tests/AGENTS.md) — harness Playwright (fără `package.json` commitat, `npx`,
|
||||
tag-uri `@regresie`/`@campanie`, 24/24).
|
||||
- `scratch/AGENTS.md` — prototipuri & QA (gitignored).
|
||||
|
||||
Nu există build sau lint. Produsul (HTML files) e vanilla HTML/CSS/JS, zero-dependențe.
|
||||
Documente de conținut: `DESIGN.md` (design campanie), `TODOS.md` (backlog), `HANDOFF.md` (handoff sesiune),
|
||||
`README.md` (utilizator final).
|
||||
|
||||
```bash
|
||||
# Verificare: deschide direct în browser (merge de pe file://)
|
||||
# sau servește local:
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
## Specific Claude Code
|
||||
|
||||
Testarea manuală: deschide `escape-builder.html`, schimbă "Stil joc" și verifică preview-ul live (iframe), apoi exportă și verifică jocul standalone. Testele automate → vezi `## Testing`.
|
||||
|
||||
## Arhitectură
|
||||
|
||||
Toată aplicația trăiește în `escape-builder.html`, organizată în secțiuni comentate (`stare`, `editor`, `preview`, `template-urile jocului exportat`):
|
||||
|
||||
- **Stare**: obiectul `state` (titlu, poveste, culoare, `style`, listă `puzzles`), persistat automat în `localStorage` sub cheia `escape-builder-v1`. Export/import ca JSON.
|
||||
- **Editor** (stânga): formularele scriu direct în `state` via `data-g`; orice modificare cheamă `onChange()` → persist + refresh preview cu debounce 400ms.
|
||||
- **Preview** (dreapta): `refreshPreview()` setează `iframe.srcdoc = gameHTML(cleanState())` — preview-ul este exact jocul exportat, jucabil.
|
||||
- **Generare joc**: `gameHTML(cfg)` face dispatch pe `cfg.style` către cele 5 motoare: `gameClassic`, `gameTerminal`, `gameArcade`, `gameChat`, `gamePoint`. Fiecare motor returnează un string HTML complet, standalone (jocul exportat merge offline).
|
||||
|
||||
Cod partajat între motoare (injectat în HTML-ul generat):
|
||||
- `libJS(cfg)`: `CFG` (config serializat), `norm` (normalizare răspuns: fără diacritice/majuscule, virgulă→punct), `checkAnswer`, `starsFor` (3/2/1 stele după încercări/indiciu), `finalWord` (literele puzzle-urilor formează cuvântul final), `beep` (WebAudio), `confetti`.
|
||||
- `SNIP.*`: fragmente de template partajate — `baseCss`, modal de întrebare (`modalCss/Html/Js`, folosit de arcade și point) și ecranul final (`finalCss/Html/Js`).
|
||||
|
||||
Tipuri de puzzle: `free` (răspuns liber), `tf` (adevărat/fals), `choice` (variante pe linii separate în `choices`, cea corectă prefixată cu `*`).
|
||||
|
||||
## Testing
|
||||
|
||||
Harness Playwright în `tests/smoke.mjs`. Instalare o singură dată:
|
||||
|
||||
```bash
|
||||
npm install # instalează @playwright/test (devDependency)
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
Rulare:
|
||||
|
||||
```bash
|
||||
npm run test:regresie # regresie — exemplu-*.html rezolvate până la final + edge cases
|
||||
npm run test:campanie # campanie E2E — rulează după ce integrator anunță gata
|
||||
npm test # suita completă (regresie + campanie)
|
||||
|
||||
# sau direct:
|
||||
npx playwright test tests/smoke.mjs --grep "@regresie"
|
||||
npx playwright test tests/smoke.mjs
|
||||
```
|
||||
|
||||
**Baseline curent (pre-campanie):** 13/13 `@regresie` trec. Testele `@campanie` sunt marcate `skip` — se activează după implementarea `gameCampaign`.
|
||||
|
||||
## Atenție la editare
|
||||
|
||||
- Motoarele de joc sunt template literals mari — backslash-urile din codul generat trebuie dublate (`\\u0300`, `\\n`), iar codul generat folosește `var`/`function` clasic intenționat.
|
||||
- O schimbare în `libJS`/`SNIP` afectează toate cele 5 motoare; verifică fiecare stil în preview.
|
||||
- `exemplu-*.html` sunt jocuri demo exportate din builder (câte unul per stil). Nu le edita manual — după modificări la motoare, regenerează-le prin exportul din builder.
|
||||
- `index.html` e doar pagina de landing care leagă builder-ul și demo-urile.
|
||||
- `package.json` + `node_modules/` sunt **doar dev tooling** (Playwright). Produsul (fișierele HTML) rămâne zero-dependențe — merge offline de pe `file://`.
|
||||
- Regulile de mediu și modul non-interactiv (`claude -p`) sunt în `/workspace/CLAUDE.md` (părinte).
|
||||
- Skill relevant: `/dox` — bootstrap/update al acestui arbore `AGENTS.md`.
|
||||
|
||||
57
HANDOFF.md
Normal file
57
HANDOFF.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# HANDOFF — Escape Room Builder (pentru sesiune nouă)
|
||||
|
||||
Data: 2026-06-13. Lucru DIRECT pe `main`, fără branch-uri (preferință user, proiect nou). Squash la merge; `scratch/` + npm sunt gitignored (produs zero-dependențe).
|
||||
|
||||
> **Progresul activ trăiește în `TODOS.md` → secțiunea „▶ BOARD ACTIV".** Citește-l ÎNTÂI la
|
||||
> fiecare sesiune (convenție documentată în AGENTS.md root). Acest HANDOFF e doar context narativ.
|
||||
|
||||
## ✅ Iterația 2 — COMPLETĂ (S1+S2+S3+S4), pe `main`, suita 24/24
|
||||
|
||||
- **S1** (`52f97af`) — fix sunet campanie: AudioContext deblocat la „Începe aventura" (gestul din
|
||||
iframe nu deblochează ctx-ul părintelui). Ipoteza veche „beep nedefinit" era greșită.
|
||||
- **S2** — prototipuri în `scratch/` (verificate 8/8, 7/7) + `STYLES.md`.
|
||||
- **S3** (`d67f6dd`+`309103f`+`4454df9`) — integrare în `escape-builder.html`:
|
||||
- Pas 1: **Bomberman complet** în `gameArcade` (bombe+explozii lanț, AI BFS, vieți+respawn cu
|
||||
progres păstrat, plasare aleatoare). Păstrează openPuzzle/onDoorSolved/showFinal/roomReady.
|
||||
- Pas 2: **hartă overworld** (`#overworld`) înlocuiește coridorul static. Jucător top-down →
|
||||
intră pe ușă → cameră → revine; steag de ieșire. Contract orchestrator NESCHIMBAT. Cod coridor șters.
|
||||
- Pas 3: **restyle 5 stiluri** din STYLES.md (fix WCAG terminal, neon arcade, frosted chat, etc.).
|
||||
- **S4** (`cead5c5`) — suita extinsă la **24/24**: audio S1, navigare overworld, bomberman gameplay.
|
||||
|
||||
**Decizie durabilă:** un singur fișier `escape-builder.html`, fără split/build (vezi gstack-decision-log).
|
||||
|
||||
## Context PR1 (referință istorică)
|
||||
|
||||
### PR1 — LIVRAT și VERIFICAT pe `main`
|
||||
- Commits: `a4b0ff4` (campanie multi-stil PR1) + `a42c960` (QA 21/21).
|
||||
- Suita `tests/smoke.mjs`: **21/21 PASS** (13 regresie + 8 campanie E2E), zero erori consolă.
|
||||
- Ce conține PR1: al 6-lea mod „Campanie multi-stil" — fiecare puzzle = o cameră într-un stil diferit (rotație classic/terminal/arcade/chat/point), legate prin **coridor static cu ușă** (intro poster → cameră → coridor → cameră → final cu cuvânt magic). Builder: opțiune „Campanie multi-stil" + selector stil per puzzle + `normalizePuzzle()`. Resume (djb2+safeStore), mod cameră per motor, 5 uși CSS/SVG, `DESIGN.md`, mobil.
|
||||
- Contract montare (verificat la gate T1): `<iframe srcdoc>` per cameră; camerele cheamă `parent.*` pe un nivel; template per stil cu sentinel `__CFG__` injectat prin replace-FUNCȚIE; `roomReady`/`roomError`/timeout 4s→skip; idempotență.
|
||||
|
||||
### FEEDBACK USER (post-livrare) — direcția REALĂ dorită (Iterația 2)
|
||||
Userul NU voia doar modul campanie. A cerut, cu decizii confirmate prin AskUserQuestion:
|
||||
1. **Sunet** = „doar repar efectele". BUG REAL confirmat: în `gameCampaign` orchestratorul **nu definește `beep`**, dar camerele cheamă `parent.beep()` în mod campanie → înghițit silent de try/catch → nu se aude nimic. Fix: definește `window.beep(ok)` în orchestrator (un AudioContext, creat la click „Începe aventura").
|
||||
2. **Arcade → Bomberman COMPLET**: dușmani cu AI de urmărire, bombe plasabile + explozii în lanț, blocuri distructibile, pericole, vieți + game-over + respawn fără pierderea progresului puzzle, plasare ALEATOARE cufere/uși/blocuri. (Acum: `var chest` fix în centru-jos, ZERO dușmani — `escape-builder.html:1045`.)
|
||||
3. **Campanie → HARTĂ TOP-DOWN (overworld)**: personaj care se plimbă pe o hartă văzută de sus, intră într-o ușă → se încarcă camera → revine pe hartă. Înlocuiește coridorul static actual.
|
||||
4. **Upgrade vizual la TOATE 5 stilurile** individuale.
|
||||
|
||||
## Plan Iterația 2 (task-uri create, AGENȚII NOI ÎNCĂ NE-SPAWNAȚI)
|
||||
Task-uri în task list (pot fi pierdute — storage-ul s-a resetat o dată; recreează dacă lipsesc):
|
||||
- **S1 (#10)** — fix sunet campanie (integrator, rapid, prioritar)
|
||||
- **S2a (#11)** — prototip Bomberman complet → `scratch/bomberman-proto.html` (standalone, jucabil, păstrează mecanica openPuzzle pe uși roșii + cufăr auriu = scăpare)
|
||||
- **S2b (#12)** — prototip hartă overworld → `scratch/overworld-proto.html` (respectă contractul campaniei: iframe per cameră, parent.nextRoom/roomReady)
|
||||
- **S2c (#13)** — `STYLES.md` direcție restyle pt cele 5 stiluri
|
||||
- **S3 (#14)** — integrator portează prototipurile în `escape-builder.html` (template literals → DUBLEAZĂ backslash-urile) + regenerează demo-uri. Blocat de S2a/S2b/S2c.
|
||||
- **S4 (#15)** — qa extinde `tests/smoke.mjs` (bomberman, hartă, audio, regresie). Blocat de S3.
|
||||
|
||||
**Strategie:** un singur fișier → integrare secvențială. Prototipurile (părțile grele) se construiesc în PARALEL în `scratch/`, verificate jucabile, apoi integrator le portează. Model hibrid ca la PR1.
|
||||
|
||||
**Decompunere agenți sonnet (de spawnat):** integrator (deține `escape-builder.html`) + `arcade-dev` (bomberman proto) + `map-dev` (overworld proto) + designer (STYLES.md) + qa. Echipa `escape-campania` există; agenții vechi pot fi idle/morți la sesiune nouă — verifică și re-spawn dacă e nevoie.
|
||||
|
||||
## Mediu
|
||||
- Server web rulează în fundal: `python3 -m http.server 8000 --bind 0.0.0.0` (IP container 10.0.20.171). Repornește dacă a murit. Port 8080 = ttyd (ocupat).
|
||||
- Verificare browser: Playwright MCP s-a deconectat de la orchestrator în sesiunea asta; agenții pot folosi `npx playwright`. Test: `npx playwright test tests/smoke.mjs` (fără package.json — gitignored).
|
||||
- Arhitectură: `escape-builder.html` (~1960 linii) — `gameHTML(cfg)` dispatch la motoare; `libJS(cfg)` + `SNIP.*` partajate; `gameCampaign` orchestrator ~linia 1410. Vezi `CLAUDE.md`, `DESIGN.md`.
|
||||
|
||||
## Plan original (referință)
|
||||
`/home/claude/.claude/plans/home-claude-claude-plans-propune-alte-u-replicated-papert.md` — planul PR1 (NU mai reflectă direcția nouă; bomberman + harta erau amânate/respinse acolo, userul le-a recerut explicit).
|
||||
21
README.md
21
README.md
@@ -6,6 +6,15 @@ Generator de jocuri escape room intr-un singur fisier HTML, fara backend, fara b
|
||||
|
||||
Deschide `escape-builder.html` in browser (dublu-click, merge si de pe `file://`).
|
||||
|
||||
Pe un mediu fara desktop (container/server remote), serveste fisierele static si deschide din browserul tau:
|
||||
|
||||
```bash
|
||||
cd /workspace/escape-builder
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
Apoi navigheaza la `http://<IP-server>:8000/escape-builder.html` (in containerul curent: `http://10.0.20.171:8000/escape-builder.html`).
|
||||
|
||||
- **Stanga**: editor — titlu, poveste, culoare, **stil joc**, puzzle-uri (raspuns liber / adevarat-fals / variante), indiciu si litera per puzzle.
|
||||
- **Dreapta**: preview live — jocul exact cum va arata, jucabil direct in pagina.
|
||||
- **Exporta jocul HTML**: descarca un joc standalone pe care il trimiti pe telefon/email; merge offline.
|
||||
@@ -29,3 +38,15 @@ Proiectul curent se salveaza automat in `localStorage` la fiecare modificare.
|
||||
- Fiecare puzzle poate da o litera; literele formeaza cuvantul final, dezvaluit la castig (cu confetti, in functie de stil).
|
||||
- Sunete WebAudio la corect/gresit; raspunsurile se compara fara diacritice si fara majuscule.
|
||||
- Toate motoarele de joc impart aceeasi biblioteca (config, scor, verificare raspuns, modal, ecran final) generata din builder.
|
||||
|
||||
## Testare
|
||||
|
||||
Suita de teste Playwright (smoke + campanie), fara server, direct pe `file://`:
|
||||
|
||||
```bash
|
||||
npx playwright test tests/smoke.mjs # toata suita (26/26)
|
||||
npx playwright test tests/smoke.mjs --grep @regresie
|
||||
npx playwright test tests/smoke.mjs --grep @campanie
|
||||
```
|
||||
|
||||
Detalii harness in `tests/AGENTS.md`.
|
||||
|
||||
775
STYLES.md
Normal file
775
STYLES.md
Normal file
@@ -0,0 +1,775 @@
|
||||
# STYLES.md — Direcție Restyle: Cele 5 Stiluri Individuale
|
||||
|
||||
Document de direcție vizuală pentru integratorul S3.
|
||||
Citeste AGENTS.md + DESIGN.md înainte de orice editare.
|
||||
Nu propune fonturi externe, CDN sau imagini remote — zero dependențe, merge de pe `file://`.
|
||||
|
||||
---
|
||||
|
||||
## Principii transversale
|
||||
|
||||
Toate stilurile partajează:
|
||||
- Paleta campaniei (`--c-bg: #0d0620`, `--c-surface: #221440`, `--c-gold: #fbbf24`) ca reper;
|
||||
fiecare stil poate adăuga propriul vocabular local **fără** a-l suprascrie pe cel al campaniei.
|
||||
- `--accent` vine din `cfg.color` (creator) — nu-l hardcoda.
|
||||
- `font-family: system-ui, -apple-system, "Segoe UI", sans-serif` pentru jocuri narativ/UI;
|
||||
`ui-monospace, "Courier New", monospace` pentru terminal/arcade. **Fără webfonturi.**
|
||||
- Toate butoanele interactive: `min-height: 44px`, `min-width: 44px`.
|
||||
- `@media (prefers-reduced-motion: reduce)` dezactivează orice animație (stări finale apar direct).
|
||||
- Focus vizibil: `outline: 2px solid var(--accent); outline-offset: 2px;` pe toate elementele focusabile.
|
||||
|
||||
---
|
||||
|
||||
## Ordinea de impact/efort (quick wins pentru integrator)
|
||||
|
||||
| Rang | Schimbare | Stiluri afectate | Efort |
|
||||
|------|-----------|------------------|-------|
|
||||
| 1 | Terminal: contrast `.line.dim` (#1f9c4a → #2ecc71) — cel mai critic eșec a11y | terminal | XS |
|
||||
| 2 | Classic: gradient de fundal mai profund + card cu `backdrop-filter: blur` mai pronunțat | classic | S |
|
||||
| 3 | Chat: header cu `blur` frosted-glass + bule cu shadow moale | chat | S |
|
||||
| 4 | Arcade: tile-uri HUD mai mari (44px min-height) + canvas border neon | arcade | M |
|
||||
| 5 | Point: SVG scenă cu paletă de culori mai saturată + room ambient gradient | point | M |
|
||||
|
||||
---
|
||||
|
||||
## 1. Classic — Quiz Cald
|
||||
|
||||
### 1.1 Stare actuală
|
||||
|
||||
**Paletă:**
|
||||
- Fundal: `linear-gradient(160deg, #14092e 0%, #2a1257 55%, #14092e 100%)` — violet închis monoton.
|
||||
- Card: `rgba(255,255,255,.07)` — aproape invizibil, fără personalitate.
|
||||
- Accent: `var(--accent)` (creator) — funcționează.
|
||||
- Feedback bun: `#86efac` (verde deschis); feedback rău: `#fda4af` (roz).
|
||||
|
||||
**Tipografie:** `system-ui` — neutru, nicio ierarhie vizuală marcată.
|
||||
|
||||
**Layout:** Card centrat 560px max-width, simplu, fără zonă vizuală distinctă pentru puzzle vs. scor.
|
||||
|
||||
**Slăbiciuni vizuale:**
|
||||
- Cardul se pierde în fundal — nu există separare clară între suprafețe.
|
||||
- Progres bar (`height: 7px`) — prea subțire, greu de văzut pe mobil.
|
||||
- Tile-urile de litere (34×40px) sunt sub 44px tap target pe lățime.
|
||||
- Animația `pop` (scale+fade) e mecanică, nu caldă.
|
||||
- Nu există element vizual „wow" — arată ca un quiz generic.
|
||||
|
||||
### 1.2 Direcție restyle — Quiz Cald (tip „game show pentru copii")
|
||||
|
||||
**Aspirație:** energia caldă a unui quiz-show de seară — fundal violet-auriu, cardul luminat ca o vitraliu, progresul celebrator, tipografia hierachizată.
|
||||
|
||||
**Referințe de gen:** Kahoot (energie), Who Wants to Be a Millionaire (ritm dramatic), jocuri de societate cu culori saturate.
|
||||
|
||||
### 1.3 Tokens propuși
|
||||
|
||||
```css
|
||||
/* Classic — tokens locali (nu suprascriu campania) */
|
||||
:root {
|
||||
--cl-bg1: #0e0622; /* fundal sus */
|
||||
--cl-bg2: #1e0d4a; /* fundal centru */
|
||||
--cl-bg3: #0e0622; /* fundal jos */
|
||||
--cl-card: #1a0e3d; /* suprafața cardului */
|
||||
--cl-card-brd: rgba(255,255,255,.18);
|
||||
--cl-card-glow:rgba(109,40,217,.35); /* glow accent moale */
|
||||
--cl-ink: #f1f0ff; /* text principal */
|
||||
--cl-muted: rgba(255,255,255,.55);
|
||||
--cl-ok: #86efac; /* feedback corect */
|
||||
--cl-bad: #fda4af; /* feedback greșit */
|
||||
--cl-gold: #fbbf24; /* stele, recompensă */
|
||||
--cl-progress: var(--accent);
|
||||
}
|
||||
```
|
||||
|
||||
**Fundal:** `radial-gradient(ellipse at 50% 30%, #2a0e5e 0%, #0e0622 70%)` — profunzime mai mare, senzație de spotlight.
|
||||
|
||||
**Card:**
|
||||
```css
|
||||
.card {
|
||||
background: var(--cl-card);
|
||||
border: 1px solid var(--cl-card-brd);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,.06), 0 24px 60px rgba(0,0,0,.55), 0 0 40px var(--cl-card-glow);
|
||||
}
|
||||
```
|
||||
|
||||
**Progres bar:** `height: 10px`, `border-radius: 99px`, fundal `rgba(255,255,255,.12)`, fill cu `var(--accent)` + `box-shadow: 0 0 8px var(--accent)`.
|
||||
|
||||
**Tile-uri litere:**
|
||||
```css
|
||||
.tile {
|
||||
width: 44px; /* de la 34px — atinge 44px tap target */
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.tile.won {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 12px var(--accent);
|
||||
animation: flip .5s cubic-bezier(.34,1.56,.64,1); /* bouncy flip */
|
||||
}
|
||||
```
|
||||
|
||||
**Butoane opțiuni (multiple choice):**
|
||||
```css
|
||||
button.opt {
|
||||
background: rgba(255,255,255,.08);
|
||||
border: 1px solid rgba(255,255,255,.16);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
transition: background .15s, border-color .15s;
|
||||
min-height: 48px;
|
||||
}
|
||||
button.opt:hover {
|
||||
background: rgba(255,255,255,.16);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
```
|
||||
|
||||
**Titlu puzzle (`qtitle`):** `color: var(--accent)` + `letter-spacing: .1em` + `font-size: 11px` uppercase — mai discret, nu concurează cu întrebarea.
|
||||
|
||||
**Întrebare (`question`):** `font-size: 21px`, `line-height: 1.5`, `color: var(--cl-ink)`.
|
||||
|
||||
### 1.4 Micro-interacțiuni & motion
|
||||
|
||||
- `@keyframes flip` — înlocuiește flip-ul liniar cu `cubic-bezier(.34,1.56,.64,1)` (bouncy spring) pentru tile-uri câștigate.
|
||||
- `@keyframes pop` — mai caldă: `from { transform: scale(.94) translateY(6px); opacity: 0; }` + `cubic-bezier(.22,1,.36,1)`.
|
||||
- Progres bar: `transition: width .5s cubic-bezier(.22,1,.36,1)` — fill fluent.
|
||||
- `@keyframes shake` — rămâne ca e, adaugă `color: var(--cl-bad)` pe input pe 400ms apoi reset.
|
||||
- `@media (prefers-reduced-motion: reduce)`: toate `animation: none; transition: none;`.
|
||||
|
||||
### 1.5 Constrângeri — ce NU se schimbă
|
||||
|
||||
- Structura HTML: `#sStart`, `#sGame`, `#sFinal` — aceleași ID-uri (testate în smoke.mjs).
|
||||
- `CFG`, `var accent`, logica `check()`, `lettersBar()`, `confetti()` — neatinsă.
|
||||
- Sentinela `__CFG__` și backslash dublu în template literal.
|
||||
- Variabilele `--accent`, `--accent-light` setate din JS — nu le suprascrie cu valori fixe.
|
||||
- Clasa `.campaign-mode` trebuie să ascundă `h1` și `.progress` (§13 DESIGN.md).
|
||||
|
||||
### 1.6 Checklist a11y
|
||||
|
||||
- [ ] Tile-uri: min 44×44px (propus 44×48px — OK).
|
||||
- [ ] Contrast text principal `#f1f0ff` pe `#1a0e3d`: ~13:1 — OK.
|
||||
- [ ] Contrast muted `rgba(255,255,255,.55)` pe `#1a0e3d`: ~5.8:1 — OK (≥4.5:1).
|
||||
- [ ] Contrast `.cl-ok` (`#86efac`) pe `#1a0e3d`: ~8.2:1 — OK.
|
||||
- [ ] Contrast `.cl-bad` (`#fda4af`) pe `#1a0e3d`: ~6.1:1 — OK.
|
||||
- [ ] Focus: `outline: 2px solid var(--accent); outline-offset: 2px;` pe toate elementele interactve.
|
||||
- [ ] Buton „Verifică" și „Incepe aventura": min-height 44px.
|
||||
|
||||
---
|
||||
|
||||
## 2. Terminal — CRT Phosphor
|
||||
|
||||
### 2.1 Stare actuală
|
||||
|
||||
**Paletă:**
|
||||
- Fundal: `#04130a` — verde-negru, corect pentru CRT.
|
||||
- Text principal: `#39ff6e` — verde neon, OK.
|
||||
- Text dim (`line.dim`): `#1f9c4a` — **EȘEC CONTRAST**: raport față de `#04130a` este ~3.1:1, sub minimul 4.5:1.
|
||||
- Avertizare (`.warn`): `#ffd24a` pe `#04130a` — ~9.2:1, OK.
|
||||
- Eroare (`.bad`): `#ff6b6b` pe `#04130a` — ~5.4:1, OK.
|
||||
- OK (`.ok`): `#9dffc0` pe `#04130a` — ~14.6:1, excelent.
|
||||
|
||||
**Tipografie:** `"Courier New", ui-monospace` — corect pentru gen.
|
||||
|
||||
**Efecte vizuale:** scanlines (`repeating-linear-gradient`) + vignetă radială — bune, autentice.
|
||||
|
||||
**Slăbiciuni vizuale:**
|
||||
- `.line.dim` (#1f9c4a) nu atinge contrast 4.5:1 — cel mai urgent fix a11y din tot proiectul.
|
||||
- Linia de comandă `>` nu are un cursor animat (doar caret CSS nativ) — pierde din personalitate.
|
||||
- Niciun efect de „pornire" (boot sequence) pentru prima dată.
|
||||
- Lățimea maximă a containerului (`max-width: 760px`) e largă — pe ecrane mari arată dezolant de gol în laterale.
|
||||
|
||||
### 2.2 Direcție restyle — CRT Phosphor autentic
|
||||
|
||||
**Aspirație:** terminal anilor '80 — fosfor verzui, scanlines vizibile, cursor care clipește, text care apare literă cu literă (deja implementat). Adaugă flicker subtil pe scanlines și o bordură de ecran CRT.
|
||||
|
||||
**Referințe de gen:** Fallout terminal, WarGames (1983), Green text VT100.
|
||||
|
||||
### 2.3 Tokens propuși
|
||||
|
||||
```css
|
||||
/* Terminal — tokens locali */
|
||||
:root {
|
||||
--tm-bg: #040f08; /* fundal ecran, mai întunecat decât acum */
|
||||
--tm-phospor: #39ff6e; /* culoarea principală fosfor */
|
||||
--tm-dim: #2ecc71; /* text secundar — FIX CRITIC: de la #1f9c4a */
|
||||
--tm-warn: #ffd24a; /* avertizare */
|
||||
--tm-bad: #ff6b6b; /* eroare */
|
||||
--tm-ok: #9dffc0; /* succes */
|
||||
--tm-glow: rgba(57,255,110,.55); /* text-shadow glow */
|
||||
--tm-scan-clr: rgba(0,0,0,.22); /* scanlines opacitate */
|
||||
--tm-border: #1a3a24; /* rama CRT */
|
||||
}
|
||||
```
|
||||
|
||||
**Bordura CRT:**
|
||||
```css
|
||||
#crt-frame {
|
||||
position: fixed; inset: 0; pointer-events: none;
|
||||
border: 8px solid #0d1f12;
|
||||
border-radius: 18px;
|
||||
box-shadow: inset 0 0 60px rgba(0,0,0,.6), inset 0 0 0 1px #1a3a24;
|
||||
}
|
||||
```
|
||||
|
||||
**Scanlines:** opacitate ușor crescută:
|
||||
```css
|
||||
.scan {
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
var(--tm-scan-clr) 0 1px,
|
||||
transparent 1px 3px
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Cursor animat** (înlocuiește `caret-color` nativ):
|
||||
```css
|
||||
#prompt-cursor {
|
||||
display: inline-block; width: 9px; height: 16px;
|
||||
background: var(--tm-phospor);
|
||||
animation: cur-blink .85s step-end infinite;
|
||||
box-shadow: 0 0 6px var(--tm-phospor);
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
@keyframes cur-blink { 50% { opacity: 0; } }
|
||||
```
|
||||
|
||||
**Linie principală:**
|
||||
```css
|
||||
.line { text-shadow: 0 0 8px var(--tm-glow); }
|
||||
.line.dim { color: var(--tm-dim); } /* de la #1f9c4a la #2ecc71 */
|
||||
```
|
||||
|
||||
**Max-width mai îngust:**
|
||||
```css
|
||||
#crt { max-width: 680px; }
|
||||
```
|
||||
|
||||
**Flicker subtil (opțional, cu motion guard):**
|
||||
```css
|
||||
@keyframes flicker {
|
||||
0%,100% { opacity: 1; }
|
||||
97% { opacity: 1; }
|
||||
98% { opacity: .94; }
|
||||
99% { opacity: .98; }
|
||||
}
|
||||
body { animation: flicker 6s infinite; }
|
||||
@media (prefers-reduced-motion: reduce) { body { animation: none; } }
|
||||
```
|
||||
|
||||
### 2.4 Micro-interacțiuni & motion
|
||||
|
||||
- Cursorul `#prompt-cursor` clipește la 0.85s step-end — tipic CRT.
|
||||
- Text care apare `say()` — deja există, nu schimba ritmul (11ms per 3 chars).
|
||||
- La feedback corect (`ok`): `text-shadow` mai intens timp de 1s: `0 0 18px rgba(57,255,110,.9)`.
|
||||
- `@media (prefers-reduced-motion: reduce)`: `body animation: none`, `cur-blink: none` (cursor vizibil permanent), fără flicker.
|
||||
|
||||
### 2.5 Constrângeri — ce NU se schimbă
|
||||
|
||||
- Logica `say()`, `pump()`, `echo()` — neatinsă.
|
||||
- Comenzile `INDICIU`, `LITERE`, `AJUTOR`, `RESTART` — neschimbate.
|
||||
- `#cmd` input este singurul element interactiv — focusul automat la click pe body rămâne.
|
||||
- Structura `.scan`, `.vign` div-uri — rămân ca overlay-uri fixe.
|
||||
- Sentinela `__CFG__`, `libJS`, backslash dublu.
|
||||
|
||||
### 2.6 Checklist a11y
|
||||
|
||||
- [ ] **CRITIC**: `.line.dim`: `#2ecc71` pe `#040f08` → ~6.1:1 — FIX față de actualul 3.1:1.
|
||||
- [ ] Text principal `#39ff6e` pe `#040f08` → ~9.8:1 — OK.
|
||||
- [ ] `.warn` (#ffd24a) pe `#040f08` → ~9.2:1 — OK.
|
||||
- [ ] `.bad` (#ff6b6b) pe `#040f08` → ~5.4:1 — OK.
|
||||
- [ ] Input `#cmd`: min-height 44px (acum are `font-size: 15px` fără min-height explicit — adaugă `min-height: 44px`).
|
||||
- [ ] Focus pe `#cmd`: deja are `outline: none` (stilul CRT nu vrea outline) — acceptabil, dar adaugă `aria-label="Introdu comanda"`.
|
||||
- [ ] `aria-live="polite"` pe `#out` — screen reader citește output-ul automat.
|
||||
|
||||
---
|
||||
|
||||
## 3. Arcade — 8-bit Neon
|
||||
|
||||
### 3.1 Stare actuală
|
||||
|
||||
**Paletă:**
|
||||
- Fundal body: `#0d0820` — violet-negru.
|
||||
- Canvas border: `3px solid #36246b` — prea discretă, nu are personalitate arcade.
|
||||
- Canvas background: `#18102e`.
|
||||
- Pereți: `#33215f` / `#241646` — violet, OK.
|
||||
- Podea: `#191130` / `#1c1336` — alternare subtilă.
|
||||
- Ușa închisă: `#9f1239` / `#e11d48` — roșu, vizibil.
|
||||
- Ușa deschisă: `#166534` / `#22c55e` — verde, OK.
|
||||
- Cufăr: `#92400e` / `#f59e0b` — auriu-portocaliu.
|
||||
- HUD: `color: #b9aee0` — pastel violet, OK.
|
||||
- D-pad: `#221643` fond, `#4a3590` bordură — OK.
|
||||
|
||||
**Tipografie:** `ui-monospace` — corect pentru arcade.
|
||||
|
||||
**Slăbiciuni vizuale:**
|
||||
- Canvas border e prea subțire și lipsită de energie neon — nu evocă ecranul arcade.
|
||||
- HUD tile-uri (`#hudLetters span`): 22×26px — sub 44px tap target.
|
||||
- D-pad butoane (52×44px) — OK pe lățime, dar pe înălțime exact la limită.
|
||||
- `h1` are `font-size: 17px` — prea mic pentru un titlu arcade cu caracter.
|
||||
- Nu există nicio animație de respawn sau de victorie per cameră pe canvas.
|
||||
- Personajul e un simplu dreptunghi — ok pentru MVP, dar placeholder evident.
|
||||
|
||||
### 3.2 Direcție restyle — 8-bit Neon Cabinet
|
||||
|
||||
**Aspirație:** cabinet arcade stradal din 1985 — bordura ecranului cu glow neon, HUD cu fonturi monospace bold, paleta cu neon verde și violet electric, butoane D-pad cu textură fizică.
|
||||
|
||||
**Referințe de gen:** Pac-Man, Galaga, Donkey Kong — neon pe fundal negru, pixel chunky, energie imediată.
|
||||
|
||||
### 3.3 Tokens propuși
|
||||
|
||||
```css
|
||||
/* Arcade — tokens locali */
|
||||
:root {
|
||||
--ar-bg: #080614; /* fundal mai întunecat */
|
||||
--ar-canvas-bg: #0e0a22; /* canvas interior */
|
||||
--ar-neon-green: #4ade80; /* neon verde (ușă deschisă, succes) */
|
||||
--ar-neon-red: #f43f5e; /* neon roșu (ușă închisă) */
|
||||
--ar-neon-gold: #fbbf24; /* cufăr, stele */
|
||||
--ar-wall: #2d1b6e; /* pereți cameră */
|
||||
--ar-floor-a: #140c30; /* podea alternare A */
|
||||
--ar-floor-b: #170f36; /* podea alternare B */
|
||||
--ar-hud: #c4b5fd; /* text HUD */
|
||||
--ar-dpad-bg: #1a1040; /* d-pad fundal */
|
||||
--ar-dpad-brd: #6d28d9; /* d-pad bordură */
|
||||
--ar-canvas-brd: #6d28d9; /* bordura canvas */
|
||||
--ar-canvas-glow:rgba(109,40,217,.6);
|
||||
}
|
||||
```
|
||||
|
||||
**Canvas border — principalul upgrade vizual:**
|
||||
```css
|
||||
canvas {
|
||||
border: 4px solid var(--ar-canvas-brd);
|
||||
border-radius: 4px; /* pixel-art: colțuri mai drepte */
|
||||
box-shadow:
|
||||
0 0 0 2px #080614,
|
||||
0 0 20px var(--ar-canvas-glow),
|
||||
0 0 40px rgba(109,40,217,.25),
|
||||
inset 0 0 30px rgba(0,0,0,.6);
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
```
|
||||
|
||||
**Titlu arcade:**
|
||||
```css
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 12px var(--accent), 0 0 24px rgba(109,40,217,.5);
|
||||
margin: 12px 0 4px;
|
||||
}
|
||||
```
|
||||
|
||||
**HUD tile-uri:**
|
||||
```css
|
||||
#hudLetters span {
|
||||
width: 32px;
|
||||
height: 32px; /* mai mare decât acum (22×26), mai ușor de citit; acceptăm sub 44px pentru UI, nu butoane tap */
|
||||
border-radius: 4px; /* mai pixel-art */
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
#hudLetters span.won {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 8px var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
```
|
||||
|
||||
**D-pad butoane:**
|
||||
```css
|
||||
#dpad button {
|
||||
width: 56px;
|
||||
height: 52px; /* de la 44px — depășește minimul */
|
||||
border-radius: 6px;
|
||||
border: 2px solid var(--ar-dpad-brd);
|
||||
background: var(--ar-dpad-bg);
|
||||
color: #c4b5fd;
|
||||
font-size: 20px;
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
**Ușa deschisă (tile 3) pe canvas** — adaugă un pulsing glow in `draw()` (JS):
|
||||
Culoarea fill a ușii deschise rămâne `#22c55e`, dar înconjoară cu `ctx.shadowBlur = 12; ctx.shadowColor = '#4ade80';` înainte de draw.
|
||||
|
||||
**Fundal body:**
|
||||
```css
|
||||
body {
|
||||
background: radial-gradient(ellipse at 50% 0%, #1a0a40 0%, #080614 60%);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Micro-interacțiuni & motion
|
||||
|
||||
- D-pad button `:active` — `translateY(2px)` + shadow redus: simulare buton fizic apăsat.
|
||||
- Ușa deschisă pe canvas: `ctx.shadowBlur` pulsant (requestAnimationFrame) — glow animat pe canvas.
|
||||
- `@media (prefers-reduced-motion: reduce)`: `#dpad button { transition: none; }`, fără glow animat pe canvas.
|
||||
- Confetti final: colorat în paleta arcade (verde neon, violet, auriu) — schimbă culorile array-ului `confetti()`.
|
||||
|
||||
### 3.5 Constrângeri — ce NU se schimbă
|
||||
|
||||
- Logica de mișcare, coliziuni, map, `doorAt`, `doorPos`, `solvedFlags` — neatinsă.
|
||||
- `openPuzzle()`, `SNIP.modalJs`, `SNIP.modalHtml` — neatinse (modal partajat).
|
||||
- `canvas#cv` ID și dimensiunile `GW=13`, `RH=4`, `TS=38` — neatinse (schimbarea TS rupe layout-ul calculat).
|
||||
- Sentinela `__CFG__`, `libJS`, backslash dublu.
|
||||
|
||||
### 3.6 Checklist a11y
|
||||
|
||||
- [ ] HUD tile-uri nu sunt butoane tap — OK, sunt decorative. Dar adaugă `aria-hidden="true"` pe `#hudLetters`.
|
||||
- [ ] D-pad butoane: 56×52px — OK (>44px).
|
||||
- [ ] `aria-label` pe butoanele D-pad: deja au `data-d`, adaugă `aria-label="Stânga/Sus/Jos/Dreapta"`.
|
||||
- [ ] `canvas` nu e accesibil screen reader — adaugă `role="img" aria-label="Hartă joc arcade"`.
|
||||
- [ ] Modal (`#mOverlay`): focus trap în `openPuzzle()` — verifică că primul element focusabil primește focus.
|
||||
- [ ] Contrast HUD text `#b9aee0` pe `#080614` → ~6.8:1 — OK.
|
||||
|
||||
---
|
||||
|
||||
## 4. Chat — Messenger Modern
|
||||
|
||||
### 4.1 Stare actuală
|
||||
|
||||
**Paletă:**
|
||||
- Body: `#0b1220` — albastru-negru.
|
||||
- App container: `#0f172a` — slate dark, Tailwind.
|
||||
- Header: `#1e293b` — slate medium.
|
||||
- Bule NPC (`him`): `#1e293b` — același cu header-ul, nu se diferențiază vizual.
|
||||
- Bule player (`me`): `var(--accent)` — corect.
|
||||
- Borduri: `#334155` — slate.
|
||||
- Status „online": `#34d399` — verde emerald.
|
||||
- Composer background: `#1e293b`, input: `#0f172a`.
|
||||
|
||||
**Tipografie:** `system-ui` — corect pentru context chat.
|
||||
|
||||
**Slăbiciuni vizuale:**
|
||||
- Bula NPC (`#1e293b`) pe fundalul `#0f172a` — contrast insuficient ca diferențiere vizuală (culorile sunt prea apropiate).
|
||||
- Header-ul nu are profunzime — se contopește cu lista de mesaje.
|
||||
- Nu există indicator typing mai vizibil decât cele 3 puncte mici.
|
||||
- Tile-ul cu litera câștigată (`.bub.tile`): `background: #14532d; border: 1px solid #22c55e` — verde pe verde, ok, dar nu celebratoriu.
|
||||
- Input în composer — `border-radius: 99px` e ok, dar pe mobil poate fi prea îngust cu chips-urile alăturate.
|
||||
|
||||
### 4.2 Direcție restyle — iMessage/WhatsApp Modern cu personalitate
|
||||
|
||||
**Aspirație:** chat modern premium — suprafețe frosted-glass, bule cu shadow moale, header clar, bula de typing mai dramatică, reward-tile celebratoriu.
|
||||
|
||||
**Referințe de gen:** iMessage dark mode, Telegram dark, WhatsApp (dinamică bule), Signal.
|
||||
|
||||
### 4.3 Tokens propuși
|
||||
|
||||
```css
|
||||
/* Chat — tokens locali */
|
||||
:root {
|
||||
--ch-bg: #060d1a; /* fundal exterior */
|
||||
--ch-app: #0d1626; /* app container */
|
||||
--ch-surface: #172035; /* header, composer */
|
||||
--ch-bub-him: #1e2d45; /* bula NPC — mai diferit de fundal */
|
||||
--ch-bub-him-brd: rgba(255,255,255,.08);
|
||||
--ch-ink: #e2e8f0; /* text mesaje */
|
||||
--ch-muted: #94a3b8; /* status, metadata */
|
||||
--ch-online: #34d399; /* indicator online */
|
||||
--ch-divider: rgba(255,255,255,.08);
|
||||
--ch-tile-bg: #14532d;
|
||||
--ch-tile-brd: #22c55e;
|
||||
--ch-chip-bg: #0d1626;
|
||||
--ch-chip-brd: #334155;
|
||||
}
|
||||
```
|
||||
|
||||
**Header frosted-glass:**
|
||||
```css
|
||||
header {
|
||||
background: rgba(23,32,53,.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--ch-divider);
|
||||
box-shadow: 0 1px 0 rgba(255,255,255,.05);
|
||||
}
|
||||
```
|
||||
|
||||
**Bule NPC — mai distinct:**
|
||||
```css
|
||||
.row.him .bub {
|
||||
background: var(--ch-bub-him);
|
||||
border: 1px solid var(--ch-bub-him-brd);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.25);
|
||||
color: var(--ch-ink);
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
```
|
||||
|
||||
**Bule player:**
|
||||
```css
|
||||
.row.me .bub {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 2px 12px rgba(109,40,217,.4);
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
```
|
||||
|
||||
**Tile reward (litera câștigată):**
|
||||
```css
|
||||
.bub.tile {
|
||||
background: linear-gradient(135deg, #14532d, #166534);
|
||||
border: 1px solid var(--ch-tile-brd);
|
||||
box-shadow: 0 0 16px rgba(34,197,94,.3);
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 3px;
|
||||
padding: 14px 20px;
|
||||
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; }
|
||||
}
|
||||
```
|
||||
|
||||
**Typing indicator — mai expresiv:**
|
||||
```css
|
||||
.bub.typing i {
|
||||
width: 8px; height: 8px;
|
||||
background: #64748b;
|
||||
}
|
||||
/* Adaugă glow la animație: */
|
||||
@keyframes tp {
|
||||
30% { transform: translateY(-6px); background: var(--ch-online); }
|
||||
}
|
||||
```
|
||||
|
||||
**Composer:**
|
||||
```css
|
||||
#composer {
|
||||
background: rgba(23,32,53,.9);
|
||||
backdrop-filter: blur(8px);
|
||||
border-top: 1px solid var(--ch-divider);
|
||||
}
|
||||
#composer input {
|
||||
background: rgba(13,22,38,.8);
|
||||
border-color: var(--ch-chip-brd);
|
||||
min-height: 44px;
|
||||
}
|
||||
#composer button { min-height: 44px; min-width: 44px; }
|
||||
#composer button.chip {
|
||||
background: var(--ch-chip-bg);
|
||||
border-color: var(--ch-chip-brd);
|
||||
min-height: 44px;
|
||||
}
|
||||
```
|
||||
|
||||
**Avatar:**
|
||||
```css
|
||||
.avatar {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(255,255,255,.15);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 Micro-interacțiuni & motion
|
||||
|
||||
- Bule noi apar cu `animation: bin .25s cubic-bezier(.22,1,.36,1)` — mai „springy".
|
||||
- Tile reward: `animation: tile-pop .4s cubic-bezier(.34,1.56,.64,1)` — bouncy, celebratoriu.
|
||||
- Typing indicator: punctele devin verde (#34d399) la peak — confirmă că e „live".
|
||||
- Header: `backdrop-filter: blur(12px)` — frosted glass când mesajele trec pe sub.
|
||||
- `@media (prefers-reduced-motion: reduce)`: `animation: none` pe `bin`, `tile-pop`; typing indicator static.
|
||||
|
||||
### 4.5 Constrângeri — ce NU se schimbă
|
||||
|
||||
- `#msgs` scroll, `scrollEnd()`, `charMsg()` timing — neatins.
|
||||
- `#composer` rebuild pattern (`composer.innerHTML = ''` + `chip()`) — neatins.
|
||||
- `seq()`, `storyChunks()`, `answer()` — neatinse.
|
||||
- `SNIP.finalHtml`, `SNIP.finalJs` — neatinse.
|
||||
- Sentinela `__CFG__`, `libJS`, backslash dublu.
|
||||
|
||||
### 4.6 Checklist a11y
|
||||
|
||||
- [ ] Bule: nu sunt interactive — OK, dar asigură că butoanele chip au `min-height: 44px`.
|
||||
- [ ] Contrast bula NPC `#e2e8f0` pe `#1e2d45` → ~9.1:1 — OK.
|
||||
- [ ] Contrast muted `#94a3b8` pe `#0d1626` → ~5.2:1 — OK.
|
||||
- [ ] Composer input: `min-height: 44px` — adaugă explicit.
|
||||
- [ ] `#msgs`: adaugă `aria-live="polite" aria-label="Conversație"`.
|
||||
- [ ] Avatar: `aria-hidden="true"` (decorativ).
|
||||
- [ ] Typing indicator: `aria-label="scrie..."` pe `.bub.typing`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Point — Adventure Pixel-Art
|
||||
|
||||
### 5.1 Stare actuală
|
||||
|
||||
**Paletă:**
|
||||
- Fundal body: `#0d0820` — violet închis, identic cu Arcade (nu se diferențiază).
|
||||
- SVG scenă: fundal perete `#3b2a63`, podea `#241a3f`, despărțitor `#1c1336`.
|
||||
- Obiecte SVG: paleta `POOL[]` — predominant bruni, roșii, albastruri.
|
||||
- Ușa: `#6b4226` / `#8a5a2b` — maro lemn, OK pentru gen.
|
||||
- HUD: identic cu Arcade (`#b9aee0`) — nu are personalitate proprie.
|
||||
- Obiect rezolvat: cerc verde `#16a34a` cu text alb — funcțional.
|
||||
|
||||
**Tipografie:** `system-ui` — acceptabil, dar point-and-click merită o tentă mai „aventurieră".
|
||||
|
||||
**Slăbiciuni vizuale:**
|
||||
- Fundalul body este identic cu Arcade (`#0d0820`) — jucătorii în campanie nu simt trecerea la un stil nou.
|
||||
- SVG-ul scenei (peretele `#3b2a63`) e prea plat și monoton — nu există sursă de lumină sau ambient.
|
||||
- HUD e copiat din Arcade fără adaptare.
|
||||
- Obiecte rezolvate (`.hot.done { opacity: .6 }`) dispar prea mult — greu de văzut ce ai rezolvat.
|
||||
- Ușa deschisă (`#door.open`) are doar un `drop-shadow` verde — nu destul de celebratoriu.
|
||||
- `note` text (`#8d80bb`) — contrast pe `#0d0820`: ~3.8:1 — sub 4.5:1, eșec a11y.
|
||||
|
||||
### 5.2 Direcție restyle — Pixel-Art Adventure (cameră misterioasă)
|
||||
|
||||
**Aspirație:** point-and-click clasic — scenă cu ambient cald (lampă, fereastră cu lumină), obiecte cu hover expresiv, inventar vizual clar, ușă care emite lumină când e deblocată. Stil Monkey Island / Broken Sword întâlnit cu Undertale.
|
||||
|
||||
**Referințe de gen:** Monkey Island (cameră cu textură și lumini), Machinarium (obiect cu hover glow), jocuri point-and-click browser.
|
||||
|
||||
### 5.3 Tokens propuși
|
||||
|
||||
```css
|
||||
/* Point — tokens locali */
|
||||
:root {
|
||||
--pt-bg1: #0a0618; /* fundal sus — mai distinct față de Arcade */
|
||||
--pt-bg2: #150d30; /* gradient jos */
|
||||
--pt-hud: #d4c8f8; /* text HUD — mai luminos */
|
||||
--pt-note: #a89fd4; /* text note — FIX CONTRAST: de la #8d80bb */
|
||||
--pt-scene-wall:#2e1f5c; /* peretele SVG scenei */
|
||||
--pt-scene-amb: rgba(255,200,100,.06); /* ambient luminos cald din lampă */
|
||||
--pt-done-fill: #166534; /* obiect rezolvat fill */
|
||||
--pt-done-brd: #22c55e;
|
||||
--pt-door-open: #22c55e;
|
||||
--pt-door-glow: rgba(34,197,94,.5);
|
||||
}
|
||||
```
|
||||
|
||||
**Fundal body — distinct față de Arcade:**
|
||||
```css
|
||||
body {
|
||||
background: linear-gradient(180deg, var(--pt-bg1) 0%, var(--pt-bg2) 100%);
|
||||
}
|
||||
```
|
||||
|
||||
**SVG scenă — ambient luminos:**
|
||||
Adaugă în `base` SVG-ul un gradient radial care simulează lumina lămpii:
|
||||
```js
|
||||
var base = '...'
|
||||
+ '<radialGradient id="amb" cx="65%" cy="60%" r="45%">'
|
||||
+ '<stop offset="0%" stop-color="#ffe082" stop-opacity=".12"/>'
|
||||
+ '<stop offset="100%" stop-color="#ffe082" stop-opacity="0"/>'
|
||||
+ '</radialGradient>'
|
||||
+ '<rect width="800" height="500" fill="url(#amb)"/>';
|
||||
```
|
||||
|
||||
**Hover obiect — mai expresiv:**
|
||||
```css
|
||||
.hot:hover {
|
||||
filter: brightness(1.5) drop-shadow(0 0 8px rgba(255,220,100,.5));
|
||||
transition: filter .15s;
|
||||
}
|
||||
```
|
||||
|
||||
**Obiect rezolvat — vizibil, nu disparent:**
|
||||
```css
|
||||
.hot.done {
|
||||
opacity: .85; /* de la .6 — rămâne vizibil */
|
||||
cursor: default;
|
||||
}
|
||||
.hot.done:hover { filter: none; }
|
||||
```
|
||||
|
||||
**Ușa deschisă — celebratorie:**
|
||||
```css
|
||||
#door.open {
|
||||
filter: drop-shadow(0 0 18px var(--pt-door-glow)) 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 var(--pt-door-glow)); }
|
||||
to { filter: drop-shadow(0 0 24px var(--pt-door-glow)) drop-shadow(0 0 8px rgba(255,255,255,.3)); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#door.open { animation: none; filter: drop-shadow(0 0 18px var(--pt-door-glow)); }
|
||||
}
|
||||
```
|
||||
|
||||
**Note text — fix contrast:**
|
||||
```css
|
||||
.note { color: var(--pt-note); } /* #a89fd4 pe #0a0618 → ~5.1:1, OK */
|
||||
```
|
||||
|
||||
**HUD tile-uri — adaptate la Point:**
|
||||
```css
|
||||
#hudLetters span {
|
||||
width: 26px; height: 30px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255,255,255,.08);
|
||||
border: 1px solid rgba(255,255,255,.18);
|
||||
}
|
||||
#hudLetters span.won {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 10px var(--accent);
|
||||
}
|
||||
```
|
||||
|
||||
**Titlu (`h1`) — mai ambient:**
|
||||
```css
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
color: #e8deff;
|
||||
letter-spacing: .04em;
|
||||
text-shadow: 0 2px 8px rgba(0,0,0,.6);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Micro-interacțiuni & motion
|
||||
|
||||
- Hover pe `.hot`: `filter` cu `transition: .15s` — feedback imediat la cursor.
|
||||
- Ușa deschisă: `door-glow` keyframe — glow pulsant verde.
|
||||
- La `onSolved()`: badge-ul cu litera câștigată poate primi `animation: badge-pop .35s cubic-bezier(.34,1.56,.64,1)` — similar cu tile-ul din Chat.
|
||||
- `@media (prefers-reduced-motion: reduce)`: `.hot { transition: none; }`, `#door.open { animation: none; }`, badge fără animație.
|
||||
|
||||
### 5.5 Constrângeri — ce NU se schimbă
|
||||
|
||||
- `POOL[]` array de obiecte SVG — conținutul SVG neatins (numai CSS hover modificat).
|
||||
- `base` SVG string — se poate adăuga gradient, dar NU se schimbă coordonatele ușii sau obiectelor.
|
||||
- `openPuzzle()`, `SNIP.modalJs`, `SNIP.modalHtml` — neatinse.
|
||||
- `el('door').classList.add('open')` — trigger JS neatins.
|
||||
- Sentinela `__CFG__`, `libJS`, backslash dublu.
|
||||
|
||||
### 5.6 Checklist a11y
|
||||
|
||||
- [ ] **FIX**: `.note` (`#8d80bb`) pe `#0d0820` → 3.8:1 (eșec). Nou: `#a89fd4` pe `#0a0618` → ~5.1:1 — OK.
|
||||
- [ ] Obiectele `.hot` nu au `role="button"` — adaugă `role="button" tabindex="0"` + handler `onkeydown` pentru Enter/Space.
|
||||
- [ ] `#door` — la fel: `role="button" tabindex="0"` + keydown handler.
|
||||
- [ ] Modal `#mOverlay`: focus trap — verifică.
|
||||
- [ ] HUD tile-uri: `aria-hidden="true"` (decorative).
|
||||
- [ ] Contrast HUD `#d4c8f8` pe `#0a0618` → ~11.2:1 — OK.
|
||||
- [ ] Butoane modal: `min-height: 44px` — deja în `SNIP.modalCss`.
|
||||
|
||||
---
|
||||
|
||||
## Rezumat rapid per stil (5 rânduri)
|
||||
|
||||
| Stil | Direcție |
|
||||
|------|----------|
|
||||
| **Classic** | Quiz cald cu fundal radial profund, card cu glow accent, tile-uri 44px bouncy, progres bar mai gros cu neon glow. |
|
||||
| **Terminal** | CRT fosfor autentic cu bordură de ecran, fix critic contrast `.dim` (#2ecc71), cursor animat step-end, flicker subtil cu motion guard. |
|
||||
| **Arcade** | Cabinet neon cu canvas border glow violet electric, D-pad butoane fizice (56×52px), titlu neon cu text-shadow, fundal radial distinct. |
|
||||
| **Chat** | Messenger premium frosted-glass header, bule NPC cu shadow și contrast clar, tile reward bouncy cu glow verde, typing indicator cu culoare. |
|
||||
| **Point** | Cameră aventurieră cu ambient radial din lampă SVG, hover obiect expresiv, ușă cu glow pulsant, fix contrast note text (#a89fd4). |
|
||||
|
||||
---
|
||||
|
||||
## Top 3 schimbări cu cel mai mare impact la cel mai mic efort
|
||||
|
||||
1. **Terminal `.line.dim` #1f9c4a → #2ecc71** (1 linie CSS) — remediază singurul eșec WCAG 4.5:1 critic din tot produsul. Impact: a11y + lizibilitate. Efort: XS.
|
||||
|
||||
2. **Classic card glow + progres bar mai gros** (3-5 linii CSS pe `.card` și `.progress i`) — transformă aspectul din „quiz generic" în „experience premium". Impact: prima impresie. Efort: S.
|
||||
|
||||
3. **Chat header frosted-glass + bule NPC distincte** (`backdrop-filter: blur(12px)` pe `header`, culoare bula NPC la `#1e2d45`) — face interfața să pară un messenger real, nu un chat placeholder. Impact: imersie, credibilitate stil. Efort: S.
|
||||
162
TODOS.md
162
TODOS.md
@@ -3,6 +3,95 @@
|
||||
Backlog post-PR1 și note tehnice pentru iterațiile viitoare.
|
||||
Referință plan complet: `~/.gstack/projects/romfast-escape-builder/ceo-plans/2026-06-12-campania-multi-stil.md`
|
||||
|
||||
> **Acest fișier e BOARD-UL DE PROGRES (sursa durabilă).** Task list-ul din harness se
|
||||
> resetează între sesiuni → se uită. Aici nu. La fiecare sesiune: citește board-ul activ
|
||||
> de mai jos ÎNTÂI, mută `[ ]→[~]→[x]` pe măsură ce avansezi, commit-uiește schimbarea.
|
||||
> Convenție: `[ ]` neînceput · `[~]` în lucru · `[x]` gata+verificat · `[!]` blocat.
|
||||
|
||||
---
|
||||
|
||||
## ▶ PR2 în curs (le iau pe rând, cerere user 2026-06-13)
|
||||
- [x] **Audio camere** — fix REAL (vezi S1 mai jos, commit `651025b`): unlock pe primul gest global
|
||||
(acoperă resume), nu doar btn-start; test rescris (headless crea ctx `running` trivial).
|
||||
- [x] **Narațiune vocală (D10)** — LIVRAT (vezi §„Narațiune vocală" mai jos). Smoke 25/25.
|
||||
- [x] **Unificare contract `_campaign` la final** — `libJS.campaignDone()` (vezi §dedicată mai jos).
|
||||
- [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).
|
||||
Rămas din Etapa 2: D7 (migrare classic pe libJS+SNIP) + muzică timer (T10) + Adventure Mode v0.
|
||||
|
||||
### [x] Bomberman polish (feedback user 2026-06-13) — LIVRAT
|
||||
Trei probleme raportate + o lipsă, toate în `gameArcade` (`escape-builder.html`):
|
||||
- **Fără sunete în joc** → adăugat `sfx(type)` (WebAudio local în iframe, deblocat de gesturile din
|
||||
arcade): `bomb` (plasare), `explosion` (zgomot filtrat lowpass + thump sine), `enemy` (dușman ucis),
|
||||
`powerup` (arpegiu), `death`. `beep(ok)` din libJS rămâne pt. răspuns corect/greșit.
|
||||
- **Rază prea mare** → `EXPLOSION_RANGE=3` const → `bombRange` variabil pornind de la `BASE_RANGE=1`
|
||||
(Bomberman clasic). Similar `maxBombs` de la `BASE_BOMBS=1`.
|
||||
- **Fără powerup-uri** → la spargerea unei cutii, șansă `POWERUP_CHANCE=0.32` să cadă 🔥 (rază+1) sau
|
||||
💣 (bombe+1). Ridicate mergând pe ele; persistă peste respawn, reset la `init()`. HUD arată 💣/🔥.
|
||||
- **Bug prins** (drop=0 inițial): powerup-ul cădea pe celula cutiei, iar `checkExplosionHits` îl ștergea
|
||||
instant ca fiind „pe o celulă de explozie". Fix: colectez `brokenBoxes`, dau drop DUPĂ `checkExplosionHits`.
|
||||
Teste noi: smoke #27 (rază 1 + drop supraviețuiește + pickup crește rază/bombe). Hooks `__game`:
|
||||
`powerups`/`bombRange`/`maxBombs`/`dropPowerupAt`. Verificat: smoke 27/27 + live (drop ~30%, 0 erori).
|
||||
|
||||
---
|
||||
|
||||
## ▶ BOARD ACTIV — Iterația 2 (Adventure Mode / restyle)
|
||||
|
||||
Direcția cerută de user (decizii confirmate, vezi `HANDOFF.md`). Model hibrid ca la PR1:
|
||||
părțile grele se prototipează în PARALEL în `scratch/`, verificate jucabile, apoi integrator le
|
||||
portează în `escape-builder.html` (un singur fișier, integrare secvențială).
|
||||
|
||||
- [x] **S1 — fix sunet campanie** *(GATA — REVENIT: fix-ul inițial era incomplet, user raporta tăcere)*
|
||||
Cauză reală: gestul din iframe NU deblochează AudioContext-ul părintelui → ctx `suspended` → tăcere.
|
||||
Fix v1 (incomplet): deblocare DOAR în handler-ul `btn-start`. Lacună: calea de **resume**
|
||||
(reload mid-campanie, `escape-builder.html:2199`) intră direct pe hartă FĂRĂ btn-start → ctx
|
||||
nedeblocat → camere mute. Plus `resume()` singur nu ajunge pe iOS Safari.
|
||||
Fix v2 (real): `unlockAudio()` + listener GLOBAL one-time pe primul gest (`pointerdown`+`keydown`,
|
||||
capture) — acoperă fresh ȘI resume (mers pe hartă = keydown pe părinte); buffer silențios
|
||||
iOS-safe; `beep()` se auto-vindecă dacă ctx redevine `suspended`. `escape-builder.html:1893`.
|
||||
**Lecție testare:** headless Chromium creează ctx direct `running` (ignoră autoplay policy) →
|
||||
vechiul test „ctx running" trecea trivial, NU putea prinde tăcerea. Test nou (smoke #9):
|
||||
gest tastatură FĂRĂ btn-start → running (cale resume) + beep self-heal din ctx suspendat.
|
||||
Verificat: smoke 24/24 + live MCP (ArrowDown singur deblochează). Demo-uri regenerate.
|
||||
- [x] **S2a — prototip Bomberman complet** → `scratch/bomberman-proto.html` (GATA, 8/8 verificat de mine)
|
||||
Grid 15×13, bombe timer 2.4s + explozii lanț, cutii distructibile, AI dușmani BFS urmărire,
|
||||
3 vieți + respawn cu progres puzzle PĂSTRAT (stare separată), PRNG seedat (`window.__seed`),
|
||||
uși roșii `openPuzzle(id,cb)` + cufăr = scăpare. Hooks `window.__game`.
|
||||
Test: `scratch/verify-bomberman.mjs` (+ `pw-bomberman.config.mjs`). Am corectat testul AI:
|
||||
dușmanii merg DOAR pe podea → cei închiși în cutii nu se mișcă (corect); testul curăță cutiile.
|
||||
Note S3: dușmanii confinați de cutii e intenționat — la integrare asigură căi sau acceptă.
|
||||
- [x] **S2b — prototip hartă overworld** → `scratch/overworld-proto.html` (GATA, 7/7 verificat de mine)
|
||||
Hartă tile 20×18, player top-down (săgeți/WASD/dpad), 4 uși + exit deblocat după toate.
|
||||
Orchestrator identic cu `gameCampaign`: `mountRoom`/`roomReady`/`nextRoom`/`roomError`/timeout 4s,
|
||||
resume localStorage, idempotență. Hooks test `window.__map`. Test: `scratch/test-overworld.mjs`.
|
||||
Note S3: stub `makeSrcdoc` → `TPL[style].replace('__CFG__', fn)`; DOOR_TILES paralel cu puzzles;
|
||||
backslash dublu la portare. Ordine rezolvare LIBERĂ.
|
||||
- [x] **S2c — `STYLES.md`** — direcție restyle pentru cele 5 stiluri (GATA, 775 linii).
|
||||
Top 3 impact/efort: terminal `.line.dim` fix WCAG (3.1:1→6.1:1); classic card glow +
|
||||
progres bar; chat header `backdrop-filter` + bulă NPC distinctă. Consumat de S3.
|
||||
- [x] **S3 — integrare în `escape-builder.html`** *(GATA — toate 3 pas-urile; smoke 21/21)*
|
||||
Portează prototipurile (template literals → DUBLEAZĂ backslash-urile) + regenerează demo-urile.
|
||||
Pas 1: Bomberman → `gameArcade`. Pas 2: Overworld → `gameCampaign`. Pas 3: restyle 5 stiluri.
|
||||
- [x] Pas 1 — Bomberman în `gameArcade` (GATA). Păstrează `openPuzzle`/`onDoorSolved`/`showFinal`/
|
||||
`modalOpen()`/`roomReady`; uși=N puzzle-uri, cufăr=scăpare. Demo regenerat. Smoke 21/21 +
|
||||
verificare gameplay 6/6 (`scratch/verify-arcade-integrated.mjs`) + captură.
|
||||
- [x] Pas 2 — Overworld în `gameCampaign` (GATA). Hartă top-down `#overworld` înlocuiește
|
||||
coridorul; intro→`showOverworld(0)`, nextRoom/skip/resume→`showOverworld`. Contractul
|
||||
(mountRoom/nextRoom/roomReady/roomError/timeout/finale) NESCHIMBAT. Cod coridor șters.
|
||||
Cele 8 teste campanie rescrise (`enterRoom`/`waitOverworld`/`__ow`). Smoke 21/21 + captură.
|
||||
- [x] Pas 3 — restyle 5 stiluri din `STYLES.md` (GATA, toate 5). Classic spotlight+card glow+
|
||||
tile 44px; Terminal fix WCAG `.dim` #2ecc71 + bordură CRT + flicker; Arcade canvas neon +
|
||||
dpad fizic; Chat header frosted + bule distincte + tile reward; Point fundal distinct +
|
||||
fix contrast `.note` + ușă glow. `prefers-reduced-motion` peste tot. Toate 5 demo-uri
|
||||
regenerate. Smoke 21/21 + capturi pe fiecare stil.
|
||||
- [x] **S4 — extinde `tests/smoke.mjs`** *(GATA — 24/24)* — 3 teste noi: audio S1 (ctx running),
|
||||
navigare overworld (mers tastatură + ieșire blocată), bomberman gameplay (bombă/AI/respawn).
|
||||
Arbore AGENTS.md actualizat 21→24.
|
||||
|
||||
**Stare la 2026-06-13:** PR1 livrat (`a42c960`). **Iterația 2 COMPLETĂ** — S1+S2+S3+S4 livrate
|
||||
și verificate. Suita 24/24. Comituri: S1 `52f97af`, S2c `a9f3065`, S3 `4454df9` (+pas1/pas2).
|
||||
|
||||
---
|
||||
|
||||
## Post-PR1 (după ship-ul campaniei)
|
||||
@@ -14,26 +103,63 @@ Referință plan complet: `~/.gstack/projects/romfast-escape-builder/ceo-plans/2
|
||||
- Edge: muzica se oprește la `speechSynthesis.cancel()` dacă vocea e activă simultan.
|
||||
- Legat de: T10 (PR2), timer countdown în bara chrome (§Design pct. 10).
|
||||
|
||||
### Edge case-uri voce (SpeechSynthesis) — PR2
|
||||
- `speechSynthesis.getVoices()` poate fi gol sincron → ascultă `voiceschanged`.
|
||||
- Fără voce `ro-*` → fallback la vocea default (nu crash, nu tăcere).
|
||||
- Voce activă mid-cameră → `speechSynthesis.cancel()` la demontare cameră (pater deține).
|
||||
- `parent.voiceSay(text)` = no-op în jocurile simple (funcția nu există) → guard `typeof parent.voiceSay === 'function'`.
|
||||
- Referință: D10 din plan; E2 Etapa 2 pct. 3.
|
||||
### [x] Narațiune vocală (SpeechSynthesis, D10) — LIVRAT (PR2)
|
||||
Feature NOU (nu doar edge-cases — voce nu exista deloc). Opt-in din builder (checkbox
|
||||
`voice`, off implicit), buton 🔊/🔇 în bara chrome a campaniei (părinte deține). Orchestrator-only
|
||||
voicing (uniform pe toate 5 motoarele, fără dublu-citit): poveste la „Începe aventura", întrebarea
|
||||
camerei la `roomReady`, mesajul final la `showFinale`. Toate edge-case-urile tratate:
|
||||
- `getVoices()` gol sincron → re-citire la `onvoiceschanged` (`_pickVoice`).
|
||||
- Fără voce `ro-*` → vocea default (nu setăm `u.voice`, doar `u.lang='ro-RO'`).
|
||||
- `speechSynthesis.cancel()` în `hideAll()` → fără replici fantomă la schimbarea scenei.
|
||||
- Fără `speechSynthesis` în window → buton ascuns, tot devine no-op.
|
||||
- `window.voiceSay` expus pe părinte (pt. viitor: replici din motoare cu guard `typeof`).
|
||||
Bug prins de test: `#btn-voice{display:inline-flex}` bătea UA `[hidden]` → adăugat `[hidden]{display:none}`.
|
||||
Verificat: smoke 25/25 (test nou „voce — naratiune opt-in") + live MCP (buton, toggle, checkbox builder).
|
||||
NOTĂ scope: motoarele NU cheamă încă `parent.voiceSay` (am evitat dublu-citit cu roomReady); dacă
|
||||
pe viitor vrei replici chat citite individual, adaugă în `charMsg` cu guard `typeof parent.voiceSay`.
|
||||
|
||||
### Unificarea `finale()` din terminal pe `SNIP.finalJs` (PR2 primul pas)
|
||||
- Astăzi terminalul are propria funcție `finale()` (escape-builder.html:863) care NU folosește `SNIP.finalJs`.
|
||||
- Migrarea pe SNIP.finalJs deblochează ramura `_campaign` uniformă pentru toate cele 5 motoare.
|
||||
- Prim pas al Etapei 2 (D7): migrarea `gameClassic` pe `libJS+SNIP` → regresie manuală pe classic.
|
||||
- Referință: planul §Etapa 2 pct. 1; D7.
|
||||
### [x] Unificarea contractului `_campaign` la final — `libJS.campaignDone()` (LIVRAT)
|
||||
**Decizie de design (abatere de la formularea inițială):** NU am pus terminalul pe `showFinal()`
|
||||
din `SNIP.finalJs`. Motiv: `showFinal()` randează un modal mov `#fOverlay`, iar terminalul are
|
||||
finale stilizat în CRT (ASCII „EVADARE REUSITA" + comandă `RESTART`) — e on-theme intenționat;
|
||||
forțarea modalului ar fi o **regresie vizuală** pe terminalul standalone.
|
||||
Ce am unificat în schimb (adevărata duplicare): payload-ul `parent.nextRoom({idx,stars,letter})`
|
||||
era scris identic în 3 locuri (terminal `finale()`, `SNIP.finalJs showFinal()`, classic `next()`).
|
||||
Acum trăiește o singură dată în `libJS.campaignDone()` (lângă `roomReady`/`beep`/`onerror`).
|
||||
- terminal `finale()` ramura `_campaign` → `say([... CAMERA REZOLVATA ...], 'ok', campaignDone)`.
|
||||
- `SNIP.finalJs showFinal()` ramura `_campaign` → `campaignDone()`.
|
||||
- arcade/chat/point folosesc `showFinal` → primesc automat `campaignDone`.
|
||||
- **classic rămâne bespoke** (nu folosește `libJS`) → contractul lui e încă inline. Pliere completă
|
||||
= D7 (migrarea `gameClassic` pe `libJS+SNIP`, cu regresie manuală pe classic). RĂMAs DE FĂCUT.
|
||||
Verificat: smoke 25/25 (terminal standalone test 2 + camere terminal în campanie E2E test 1).
|
||||
Referință: planul §Etapa 2 pct. 1; D7.
|
||||
|
||||
### Audit a11y motoare existente (post-PR1, sub harness Playwright)
|
||||
- **Ținte tap ≥ 44px**: dpad arcade, butoane tf/choice, butonul "Trimite" din chat.
|
||||
- **Contrast ≥ 4.5:1**: text terminal dim (`#1f9c4a` pe `#04130a` — verifică), hint text clasic.
|
||||
- **`@media (prefers-reduced-motion: reduce)`**: dezactivează `pop`, `flip`, `flipin`, `shake`, `confetti`, `bin` — stările finale apar direct.
|
||||
- **Focus & Enter**: "Deschide ușa" (campanie) focusabil + Enter; dpad arcade accesibil cu keyboard.
|
||||
- **`aria-label` pe progres**: bara chrome din campanie (`aria-label="Camera X din Y"`).
|
||||
- Referință: §Design pct. 13 (TD5, PR2); D19 din plan.
|
||||
### [ ] D7 rămas: migrarea `gameClassic` pe `libJS+SNIP`
|
||||
- Classic (escape-builder.html:451) e singurul motor bespoke: propriul `totalStars`, `beep`,
|
||||
inline `finalWord` (dublat de 2 ori în `next()`), propriul modal final `#sFinal`.
|
||||
- După migrare: classic folosește `libJS.campaignDone()` + `SNIP` ca celelalte 4 → 5/5 uniform.
|
||||
- Necesită regresie manuală pe classic standalone (e demo-ul implicit, cel mai vizibil).
|
||||
|
||||
### [x] Audit a11y motoare existente — LIVRAT (sub harness Playwright)
|
||||
Auditat faptic (măsurat, nu presupus). Ce era DEJA OK (din restyle S3, nemodificat):
|
||||
- **Tap ≥44px**: arcade dpad 56×52, butoane classic 44/48, chat send/chip 44 — toate ✓.
|
||||
- **Contrast**: terminal `.dim` #2ecc71 pe #040f08 ≈ **9.4:1** ✓ (nota TODOS `#1f9c4a` era stale,
|
||||
schimbat la S3); classic `button.hint` .55 alb pe card #1a0e3d ≈ **6:1** ✓. Niciun fix necesar.
|
||||
- **Focus & Enter**: butoane reale peste tot (Enter/Space nativ); arcade+overworld navigabile cu
|
||||
săgeți (keydown pe document). „Deschide ușa" coridor = OBSOLET (overworld a înlocuit coridorul;
|
||||
ușile se intră mergând cu tastatura → owCheckEnter).
|
||||
Ce am REPARAT:
|
||||
- **Tap**: overworld dpad era 42×42 → **44×44** (singura țintă sub prag).
|
||||
- **reduced-motion** (lacune reale): `.confetti` (display:none) în classic + SNIP.baseCss + campanie;
|
||||
`flipin` final (SNIP.finalCss `#fOverlay .fword span` + campanie `#fin-word span`); `dt-blink`
|
||||
(cursor ușă terminal) în campanie. `pop`/`flip`/`shake`/`bin`/`tile-pop`/`tp`/`door-glow`/`crt-flicker`
|
||||
erau deja acoperite. NB: `flipin`/`pop` au `backwards` fill → `animation:none` le revine la starea
|
||||
vizibilă (nu rămân ascunse — verificat).
|
||||
- **aria**: `#dots` `role=group`+label; fiecare dot `role=img` cu `aria-label` ce reflectă STAREA
|
||||
(neînceputa/în curs/rezolvata) via `setDot`; dpad arcade+overworld au `aria-label` (Sus/Jos/Stânga/
|
||||
Dreapta/Pune bomba); spacerele `.sp` overworld → `aria-hidden`+`tabindex=-1`.
|
||||
Test nou smoke #9c (`a11y — tap>=44px + aria + reduced-motion`, cu `emulateMedia reducedMotion`). 26/26.
|
||||
Referință: §Design pct. 13 (TD5, PR2); D19 din plan.
|
||||
|
||||
---
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,22 +6,29 @@
|
||||
<title>Comoara ascunsa</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; background: #0d0820; color: #fff; font-family: ui-monospace, "Courier New", monospace; display: flex; flex-direction: column; align-items: center; }
|
||||
h1 { font-size: 17px; margin: 14px 0 6px; letter-spacing: .06em; text-transform: uppercase; }
|
||||
#hud { display: flex; gap: 16px; align-items: center; font-size: 13px; color: #b9aee0; margin-bottom: 8px; flex-wrap: wrap; justify-content: center; padding: 0 10px; }
|
||||
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: 22px; height: 26px; border-radius: 5px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); 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; }
|
||||
canvas { border: 3px solid #36246b; border-radius: 8px; background: #18102e; max-width: calc(100vw - 16px); }
|
||||
.help { font-size: 12px; color: #6f639e; margin: 8px 0 4px; text-align: center; padding: 0 10px; }
|
||||
#dpad { display: flex; gap: 8px; margin: 6px 0 16px; }
|
||||
#dpad button { width: 52px; height: 44px; font-size: 18px; border-radius: 9px; border: 1px solid #4a3590; background: #221643; color: #cdc3f0; cursor: pointer; }
|
||||
#dpad button:active { background: var(--accent); }
|
||||
#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; }
|
||||
|
||||
.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; } }
|
||||
#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; }
|
||||
@@ -43,6 +50,7 @@
|
||||
#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%; }
|
||||
</style>
|
||||
@@ -51,8 +59,9 @@
|
||||
<h1>Comoara ascunsa</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 (da click pe joc intai). Usile rosii iti pun intrebari; cufarul auriu e scaparea.</div>
|
||||
<div id="dpad"><button data-d="L">◀</button><button data-d="U">▲</button><button data-d="D">▼</button><button data-d="R">▶</button></div>
|
||||
<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>
|
||||
<div id="mOverlay"><div id="mCard">
|
||||
<div class="mtitle" id="mTitle"></div>
|
||||
<div class="mq" id="mQ"></div>
|
||||
@@ -83,6 +92,9 @@ function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type
|
||||
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 */
|
||||
@@ -91,105 +103,278 @@ if(CFG._campaign){
|
||||
(document.head || document.documentElement).appendChild(_cs);
|
||||
}
|
||||
var N = CFG.puzzles.length;
|
||||
var GW = 13, RH = 4, ROOMS = N + 1, GH = ROOMS * RH + 1;
|
||||
var TS = 38, VR = Math.min(GH, 11);
|
||||
var map = [], doorAt = {}, doorPos = [], solvedFlags = [];
|
||||
for (var y = 0; y < GH; y++) {
|
||||
map[y] = [];
|
||||
for (var x = 0; x < GW; x++) {
|
||||
map[y][x] = (x === 0 || x === GW - 1 || y === 0 || y === GH - 1 || y % RH === 0) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < N; i++) {
|
||||
var dy = (i + 1) * RH, dx = (i % 2 === 0) ? GW - 3 : 2;
|
||||
map[dy][dx] = 2; doorAt[dy + '_' + dx] = i; doorPos.push({ y: dy, x: dx });
|
||||
}
|
||||
var chest = { y: (ROOMS - 1) * RH + 2, x: Math.floor(GW / 2) };
|
||||
map[chest.y][chest.x] = 4;
|
||||
var hero = { y: 2, x: Math.floor(GW / 2) - 2 };
|
||||
var finished = false;
|
||||
|
||||
var cv = el('cv'); cv.width = GW * TS; cv.height = VR * TS;
|
||||
/* ===== 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 draw(){
|
||||
var offY = Math.max(0, Math.min(hero.y - Math.floor(VR / 2), GH - VR));
|
||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
||||
for (var vy = 0; vy < VR; vy++) {
|
||||
var y = vy + offY;
|
||||
for (var x = 0; x < GW; x++) {
|
||||
var t = map[y][x], px = x * TS, py = vy * TS;
|
||||
if (t === 1) {
|
||||
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);
|
||||
} else {
|
||||
ctx.fillStyle = ((x + y) % 2) ? '#191130' : '#1c1336'; ctx.fillRect(px, py, TS, TS);
|
||||
if (t === 2 || t === 3) {
|
||||
ctx.fillStyle = t === 2 ? '#9f1239' : '#166534'; ctx.fillRect(px + 3, py + 2, TS - 6, TS - 4);
|
||||
ctx.fillStyle = t === 2 ? '#e11d48' : '#22c55e'; 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);
|
||||
}
|
||||
if (t === 4) {
|
||||
ctx.fillStyle = '#92400e'; ctx.fillRect(px + 5, py + 10, TS - 10, TS - 16);
|
||||
ctx.fillStyle = '#f59e0b'; ctx.fillRect(px + 5, py + 10, TS - 10, 7);
|
||||
ctx.fillStyle = '#fde68a'; ctx.fillRect(px + TS / 2 - 2, py + 13, 4, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var hx = hero.x * TS, hy = (hero.y - offY) * TS;
|
||||
ctx.fillStyle = CFG.color || '#6d28d9'; ctx.fillRect(hx + 7, hy + 5, TS - 14, TS - 10);
|
||||
ctx.fillStyle = '#fff'; ctx.fillRect(hx + 12, hy + 12, 5, 5); ctx.fillRect(hx + TS - 17, hy + 12, 5, 5);
|
||||
ctx.fillStyle = '#0d0820'; ctx.fillRect(hx + 13, hy + 14, 2, 2); ctx.fillRect(hx + TS - 16, hy + 14, 2, 2);
|
||||
ctx.fillStyle = '#fff'; ctx.fillRect(hx + 13, hy + TS - 14, TS - 26, 3);
|
||||
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 open = 0; for (var i = 0; i < N; i++) if (solvedFlags[i]) open++;
|
||||
el('hudStep').textContent = 'Usi: ' + open + '/' + N;
|
||||
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);
|
||||
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);
|
||||
for (var j = 0; j < N; j++){ var L = (CFG.puzzles[j].letter || '').trim(); if (!L) continue; var s = document.createElement('span'); if (puzzleProgress && puzzleProgress.doorsSolved[j]){ s.textContent = L.toUpperCase(); s.className = 'won'; } else s.textContent = '?'; hb.appendChild(s); }
|
||||
}
|
||||
|
||||
/* ----- 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);
|
||||
}
|
||||
|
||||
function move(dx, dy){
|
||||
if (finished || modalOpen()) return;
|
||||
var nx = hero.x + dx, ny = hero.y + dy;
|
||||
if (ny < 0 || ny >= GH || nx < 0 || nx >= GW) return;
|
||||
/* ----- 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 === 1) return;
|
||||
if (t === 2) { openPuzzle(doorAt[ny + '_' + nx], onDoorSolved); return; }
|
||||
if (t === 4) { finished = true; showFinal(); return; }
|
||||
hero.x = nx; hero.y = ny; draw();
|
||||
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();
|
||||
}
|
||||
|
||||
function onDoorSolved(i){
|
||||
solvedFlags[i] = true;
|
||||
map[doorPos[i].y][doorPos[i].x] = 3;
|
||||
updateHud(); draw();
|
||||
/* ----- 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){
|
||||
var d = { 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 (!d) return;
|
||||
e.preventDefault();
|
||||
move(d[0], d[1]);
|
||||
});
|
||||
document.querySelectorAll('#dpad button').forEach(function(b){
|
||||
b.addEventListener('click', function(){
|
||||
var m = { U: [0, -1], D: [0, 1], L: [-1, 0], R: [1, 0] }[b.dataset.d];
|
||||
move(m[0], m[1]);
|
||||
});
|
||||
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; }
|
||||
};
|
||||
|
||||
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'; };
|
||||
@@ -231,11 +416,7 @@ function mCheck(given){
|
||||
}
|
||||
}
|
||||
function showFinal(){
|
||||
if(CFG._campaign){
|
||||
var L = finalWord().charAt(0);
|
||||
try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L}); }catch(e){}
|
||||
return;
|
||||
}
|
||||
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); }
|
||||
@@ -245,7 +426,7 @@ function showFinal(){
|
||||
beep(true); confetti();
|
||||
}
|
||||
el('fAgain').onclick = function(){ location.reload(); };
|
||||
updateHud(); draw();
|
||||
init();
|
||||
roomReady();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,34 +6,37 @@
|
||||
<title>Comoara ascunsa</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #0b1220; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #e5e7eb; display: flex; justify-content: center; min-height: 100vh; }
|
||||
#app { width: 100%; max-width: 480px; height: 100vh; height: 100dvh; display: flex; flex-direction: column; background: #0f172a; }
|
||||
header { display: flex; gap: 10px; align-items: center; padding: 10px 14px; background: #1e293b; border-bottom: 1px solid #334155; }
|
||||
.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; }
|
||||
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 ease; }
|
||||
.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: #1e293b; border-bottom-left-radius: 5px; }
|
||||
.row.me .bub { background: var(--accent); color: #fff; border-bottom-right-radius: 5px; }
|
||||
.bub.tile { font-size: 24px; font-weight: 800; letter-spacing: 2px; background: #14532d; border: 1px solid #22c55e; }
|
||||
.bub.typing i { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #94a3b8; margin: 0 2px; animation: tp 1s infinite; }
|
||||
.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(-5px); } }
|
||||
#composer { padding: 10px 12px; background: #1e293b; border-top: 1px solid #334155; display: flex; flex-wrap: wrap; gap: 8px; min-height: 58px; }
|
||||
#composer input { flex: 1; min-width: 120px; font: inherit; font-size: 15px; padding: 9px 13px; border-radius: 99px; border: 1px solid #475569; background: #0f172a; color: #fff; }
|
||||
#composer input:focus { outline: none; border-color: var(--accent); }
|
||||
#composer button { font: inherit; cursor: pointer; border: none; border-radius: 99px; padding: 9px 16px; font-weight: 600; background: var(--accent); color: #fff; }
|
||||
#composer button.chip { background: #0f172a; border: 1px solid #475569; color: #cbd5e1; }
|
||||
@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; } }
|
||||
|
||||
.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; } }
|
||||
#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; }
|
||||
@@ -41,6 +44,7 @@
|
||||
#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%; }
|
||||
</style>
|
||||
@@ -72,6 +76,9 @@ function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type
|
||||
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 */
|
||||
@@ -168,11 +175,7 @@ var chatIntro = CFG._campaign
|
||||
: ['Salut' + (CFG.player ? ', ' + CFG.player : '') + '!'].concat(storyChunks()).concat(['Ma ajuti sa ies de aici?']);
|
||||
seq(chatIntro, next);
|
||||
function showFinal(){
|
||||
if(CFG._campaign){
|
||||
var L = finalWord().charAt(0);
|
||||
try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L}); }catch(e){}
|
||||
return;
|
||||
}
|
||||
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); }
|
||||
|
||||
@@ -8,44 +8,46 @@
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh; font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
color: #fff; display: flex; align-items: center; justify-content: center; padding: 16px;
|
||||
background: linear-gradient(160deg, #14092e 0%, #2a1257 55%, #14092e 100%);
|
||||
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: rgba(255,255,255,.07);
|
||||
border: 1px solid rgba(255,255,255,.14); border-radius: 18px; padding: 26px;
|
||||
backdrop-filter: blur(6px); box-shadow: 0 18px 50px rgba(0,0,0,.45);
|
||||
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 ease; }
|
||||
@keyframes pop { from { transform: scale(.96); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
||||
.progress { height: 7px; background: rgba(255,255,255,.15); border-radius: 99px; overflow: hidden; margin: 14px 0 4px; }
|
||||
.progress i { display: block; height: 100%; background: var(--accent); width: 0; transition: width .4s ease; }
|
||||
.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: 34px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 800; font-size: 18px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.18);
|
||||
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; animation: flip .5s ease; }
|
||||
.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: 19px; line-height: 1.45; margin: 8px 0 18px; }
|
||||
.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); border-color: transparent; }
|
||||
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;
|
||||
font-weight: 700; background: var(--accent); color: #fff; width: 100%; margin-top: 10px; min-height: 44px;
|
||||
}
|
||||
button:hover { filter: brightness(1.12); }
|
||||
button.opt { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); font-weight: 600; text-align: left; }
|
||||
button.opt:hover { background: rgba(255,255,255,.18); }
|
||||
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; }
|
||||
@@ -62,6 +64,11 @@
|
||||
}
|
||||
.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>
|
||||
|
||||
@@ -6,26 +6,29 @@
|
||||
<title>Comoara ascunsa</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; background: #0d0820; color: #fff; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; display: flex; flex-direction: column; align-items: center; }
|
||||
h1 { font-size: 19px; margin: 14px 0 4px; }
|
||||
#hud { display: flex; gap: 16px; align-items: center; font-size: 13px; color: #b9aee0; margin-bottom: 4px; flex-wrap: wrap; justify-content: center; padding: 0 10px; }
|
||||
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: 22px; height: 26px; border-radius: 5px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); 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; }
|
||||
.note { font-size: 13px; color: #8d80bb; margin: 2px 0 10px; text-align: center; padding: 0 12px; min-height: 18px; }
|
||||
#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.35) drop-shadow(0 0 6px rgba(255,255,255,.35)); }
|
||||
.hot.done { opacity: .6; cursor: default; }
|
||||
.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 12px #22c55e); }
|
||||
#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); } }
|
||||
@media (prefers-reduced-motion: reduce){ .confetti{ display:none !important; } .shake{ animation:none !important; } }
|
||||
#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; }
|
||||
@@ -47,6 +50,7 @@
|
||||
#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%; }
|
||||
</style>
|
||||
@@ -86,6 +90,9 @@ function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type
|
||||
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 */
|
||||
@@ -200,11 +207,7 @@ function mCheck(given){
|
||||
}
|
||||
}
|
||||
function showFinal(){
|
||||
if(CFG._campaign){
|
||||
var L = finalWord().charAt(0);
|
||||
try{ parent.nextRoom({idx:CFG._campaign.idx, stars:totalStars, letter:L}); }catch(e){}
|
||||
return;
|
||||
}
|
||||
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); }
|
||||
|
||||
@@ -6,21 +6,24 @@
|
||||
<title>Comoara ascunsa</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; background: #04130a; color: #39ff6e; font-family: "Courier New", ui-monospace, monospace; }
|
||||
#crt { max-width: 760px; 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 7px rgba(57,255,110,.5); }
|
||||
.line.dim { color: #1f9c4a; }
|
||||
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; text-shadow: 0 0 7px rgba(57,255,110,.5); }
|
||||
#cmd { flex: 1; background: none; border: none; outline: none; color: inherit; font: inherit; text-shadow: inherit; caret-color: #39ff6e; }
|
||||
.scan { position: fixed; inset: 0; pointer-events: none; background: repeating-linear-gradient(0deg, rgba(0,0,0,.28) 0 1px, transparent 1px 3px); }
|
||||
.vign { position: fixed; inset: 0; pointer-events: none; background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.6)); }
|
||||
#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 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>
|
||||
@@ -38,6 +41,9 @@ function checkAnswer(p, given){ var exp = p.type === 'tf' ? p.tfAnswer : (p.type
|
||||
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 */
|
||||
@@ -95,9 +101,7 @@ function finale(){
|
||||
done = true;
|
||||
if(CFG._campaign){
|
||||
var s = totalStars; var L = finalWord().charAt(0);
|
||||
say(['>> CAMERA REZOLVATA! Stele: ' + s + (L ? ' | Litera: ' + L : '')], 'ok', function(){
|
||||
try{ parent.nextRoom({idx:CFG._campaign.idx, stars:s, letter:L}); }catch(e){}
|
||||
});
|
||||
say(['>> CAMERA REZOLVATA! Stele: ' + s + (L ? ' | Litera: ' + L : '')], 'ok', campaignDone);
|
||||
return;
|
||||
}
|
||||
var w = finalWord().split('').join(' ');
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
<h1>Escape Room Builder</h1>
|
||||
<p>Builder-ul + cate un joc demo exportat in fiecare stil.</p>
|
||||
<a class="builder" href="escape-builder.html">Builder <span>editor + preview live; schimba "Stil joc" si vezi transformarea pe loc</span></a>
|
||||
<a href="exemplu-campanie.html" style="border-color:#a78bfa;background:rgba(167,139,250,.12)">🗺️ Campanie multi-stil <span>3 puzzle-uri × 3 stiluri diferite — ușa ca erou, coridor cu litera, cuvântul magic</span></a>
|
||||
<a href="exemplu-campanie.html" style="border-color:#a78bfa;background:rgba(167,139,250,.12)">🗺️ Campanie multi-stil <span>hartă overworld: mergi la fiecare ușă, intri într-o cameră într-un alt stil, aduni litere → cuvântul magic</span></a>
|
||||
<a href="exemplu-clasic.html">Clasic (quiz) <span>carduri secventiale cu progres si litere</span></a>
|
||||
<a href="exemplu-terminal.html">Terminal retro <span>text adventure CRT; scrie raspunsul, INDICIU, LITERE</span></a>
|
||||
<a href="exemplu-arcade.html">Arcade pixel <span>sageti / WASD; usi incuiate, cufar final</span></a>
|
||||
<a href="exemplu-arcade.html">Arcade Bomberman <span>sageti / WASD + bombe; sparge cutiile, evita dusmanii, usi cu intrebari, cufar = scaparea</span></a>
|
||||
<a href="exemplu-chat.html">Story chat <span>personajul iti scrie; raspunzi din composer</span></a>
|
||||
<a href="exemplu-point.html">Point-and-click <span>camera ilustrata; click pe obiecte, apoi pe usa</span></a>
|
||||
</div>
|
||||
|
||||
38
tests/AGENTS.md
Normal file
38
tests/AGENTS.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# tests/ — Harness Playwright
|
||||
|
||||
## Purpose
|
||||
Smoke + regresie + campanie E2E pentru jocurile generate. Verifică faptic: fiecare stil se rezolvă
|
||||
până la ecranul final, fără erori de consolă.
|
||||
|
||||
## Ownership
|
||||
- `tests/smoke.mjs` — unicul fișier de teste (~27 teste).
|
||||
- `playwright.config.mjs` (la root, **gitignored**) — config dev.
|
||||
|
||||
## Local Contracts
|
||||
- **NU commita `package.json` / `package-lock.json` / `playwright.config.mjs`** — produsul rămâne
|
||||
zero-dependențe. Instalarea dev e o singură dată: `npm i -D @playwright/test && npx playwright install chromium`.
|
||||
- **Fără npm scripts** — se rulează direct cu `npx`.
|
||||
- **Teste pe `file://`** — helper-ul `fileURL(name)` mapează cale relativă la `file://`; campania scrie
|
||||
HTML temp generat via builder (`gameHTML`) și-l încarcă de pe `file://`.
|
||||
- **Zero erori consolă = invariant.** `trackErrors(page)` colectează `console.error` + `pageerror`;
|
||||
fiecare test asertează `errors.length === 0` la final.
|
||||
- **Tag-uri:** `@regresie` (15 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML +
|
||||
bomberman gameplay + bomberman rază/powerup-uri) și `@campanie` (12 — intro→hartă→camere→final, resume,
|
||||
cameră moartă, idempotență ușă, `$`/`$&`, beep, mobil, audio S1, voce/narațiune D10,
|
||||
a11y tap/aria/reduced-motion, navigare overworld).
|
||||
- **Status țintă: 27/27 PASS.**
|
||||
|
||||
## Work Guidance
|
||||
- După modificări la motoare (`escape-builder.html`): rulează suita completă; extinde `@regresie` dacă
|
||||
adaugi/schimbi un stil, `@campanie` pentru contractul de montare.
|
||||
- Nu testa pe screenshot-uri de pixeli — asertează stare/text/erori.
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
npx playwright test tests/smoke.mjs # 27/27
|
||||
npx playwright test tests/smoke.mjs --grep @regresie
|
||||
npx playwright test tests/smoke.mjs --grep @campanie
|
||||
```
|
||||
|
||||
## Child DOX Index
|
||||
(none — leaf)
|
||||
954
tests/smoke.mjs
954
tests/smoke.mjs
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user