diff --git a/backend/src/controllers/customSimController.js b/backend/src/controllers/customSimController.js index 67f188b..03adeb9 100644 --- a/backend/src/controllers/customSimController.js +++ b/backend/src/controllers/customSimController.js @@ -275,10 +275,18 @@ function validateSpec(spec) { clean.goal = cg; } - // game{} — зарезервированный блок мета-слоя (Фаза 1/5). Пропускаем как есть - // (проверен общими лимитами: размер/глубина). Не исполняем. + // game{} — мета-слой игрового уровня (Фаза 1/5). Санитизируем ПОИМЁННО (как goal): + // строки → sanitizeText (escape), числа → проверка типа, неизвестные ключи отбрасываем. + // Иначе произвольная строка в game.* стала бы хранимым XSS у любого, кому раздали уровень. if (spec.game && typeof spec.game === 'object' && !Array.isArray(spec.game)) { - clean.game = spec.game; + const gm = spec.game; + const cgm = {}; + if (gm.chapter !== undefined) cgm.chapter = sanitizeText(gm.chapter, 60); + if (gm.subject !== undefined) cgm.subject = sanitizeText(gm.subject, 60); + if (typeof gm.order === 'number') cgm.order = gm.order; + if (typeof gm.par_ms === 'number') cgm.par_ms = gm.par_ms; + if (typeof gm.unlockStars === 'number') cgm.unlockStars = gm.unlockStars; + clean.game = cgm; } if (errs.length) return { ok: false, error: errs.slice(0, 8).join('; ') }; diff --git a/frontend/js/game/progress-logic.js b/frontend/js/game/progress-logic.js index 2ce8883..e1a34ca 100644 --- a/frontend/js/game/progress-logic.js +++ b/frontend/js/game/progress-logic.js @@ -46,11 +46,10 @@ } /* ── Разблокировка ──────────────────────────────────────────────────────── - Уровень открыт, если СУММА звёзд во ВСЕХ предыдущих уровнях той же главы - (по полю order) ≥ level.unlockStars. Первый уровень главы (минимальный order - или unlockStars==0) открыт всегда. Глава открывается, если открыт её первый - уровень — он гейтится суммой звёзд предыдущих глав через unlockStars==0 - первого уровня (по умолчанию) ИЛИ явным порогом. + Уровень открыт, если СУММА звёзд во ВСЕХ уровнях с меньшим ГЛОБАЛЬНЫМ order + (по всем главам, не только текущей) ≥ level.unlockStars. Уровень с + unlockStars==0 (или без него) открыт всегда. Так первый уровень главы + гейтится суммой звёзд всех предыдущих глав через свой порог unlockStars. Чистая функция: вход — уровень + карта прогресса + ВЕСЬ список (для подсчёта «предыдущих» по order). Возвращает bool. */ diff --git a/frontend/quantik.html b/frontend/quantik.html index e794e0b..9286956 100644 --- a/frontend/quantik.html +++ b/frontend/quantik.html @@ -220,6 +220,18 @@ .qg-star-svg { filter: drop-shadow(0 2px 6px rgba(251,191,36,0.4)); } .qg-star-on { animation: qgStarPop .45s cubic-bezier(.22,1.3,.4,1) backwards; animation-delay: calc(.12s * var(--si, 0) + .15s); } @keyframes qgStarPop { 0% { transform: scale(0) rotate(-30deg); opacity: 0; } 70% { transform: scale(1.25) rotate(6deg); } 100% { transform: scale(1) rotate(0); opacity: 1; } } + /* Доступность: уважаем prefers-reduced-motion. Делаем анимации мгновенными + (а не выключаем) — иначе forwards-анимация появления узлов (.qm-in/qmNodeIn) + не применит конечное состояние и узлы останутся скрытыми. Циклы (пульс/мерцание/ + покачивание) при этом фактически останавливаются (1 итерация мгновенно). */ + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: .01ms !important; + animation-iteration-count: 1 !important; + transition-duration: .01ms !important; + scroll-behavior: auto !important; + } + } .qg-stats { display: flex; justify-content: center; gap: 22px; margin-bottom: 20px; } .qg-stat { display: flex; flex-direction: column; gap: 3px; } .qg-stat-lbl { font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: #8B9AAE; } diff --git a/plans/quantik-game/PLAN.md b/plans/quantik-game/PLAN.md index 53cb101..2c64212 100644 --- a/plans/quantik-game/PLAN.md +++ b/plans/quantik-game/PLAN.md @@ -64,7 +64,7 @@ - [x] Phase 3: Граф-уровни (движение по f(x)) + зоны-препятствия [domain: fullstack] → [subplan](./phase-3-graph-levels.md) - [x] Phase 4: Квантовые способности + SR-комнаты [domain: fullstack] → [subplan](./phase-4-quantum-abilities-sr.md) - [x] Phase 5: Авторинг уровней в sim-builder + раздача классу [domain: fullstack] → [subplan](./phase-5-authoring-sharing.md) -- [ ] Phase 6: Класс-лидерборд / живая гонка (classroom SSE) [domain: fullstack] → [subplan](./phase-6-leaderboard-live.md) +- ~~Phase 6: Класс-лидерборд / живая гонка (classroom SSE)~~ — **REMOVED** (см. Amendment 1) → [subplan](./phase-6-leaderboard-live.md) ## Phase Progress Log @@ -76,16 +76,32 @@ | Phase 3: Граф-уровни + зоны | fullstack | ✅ Done | ✅ | ✅ | ✅ | | Phase 4: Квантовые способности + SR | fullstack | ✅ Done | ✅ | ✅ | ✅ | | Phase 5: Авторинг + раздача | fullstack | ✅ Done | ✅ | ✅ | ✅ | -| Phase 6: Лидерборд / живая гонка | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| ~~Phase 6: Лидерборд / живая гонка~~ | fullstack | ❌ Removed (Amendment 1) | — | — | — | ## MVP boundary После **Phase 2** игра играбельна и отгружаема: один полный мир физ-уровней с картой, прогрессом, XP и скинами. Фазы 3–6 — расширение (новые типы уровней, способности, авторинг, мультиплеер). +## Amendment Log + +### Amendment 1 — 2026-06-14 +**Type:** Removed phase +**What changed:** Phase 6 (Класс-лидерборд / живая гонка через classroom SSE) убрана из объёма по решению пользователя. +**Why:** Пользователь решил не реализовывать соревновательный слой; переходим к полировке и финальному ревью после Ф5. +**Impact on existing phases:** Нет. Фазы 0–5 самодостаточны и отгружаемы. `game_progress.level_id` (TEXT) уже готов под будущий лидерборд, если фичу вернут. Subplan `phase-6-leaderboard-live.md` сохранён как архив с пометкой REMOVED. + ## Final Review -- [ ] Comprehensive code review (final-reviewer) -- [ ] Security review (новые API: прогресс/лидерборд, user-input) -- [ ] `npm test` без новых регрессий (поверх baseline) -- [ ] `npm run lint:routes` baseline 0 -- [ ] Merged to `feature/sim-builder` +- [x] Comprehensive code review (final-reviewer) — ✅ READY TO MERGE, 0 блокеров (2026-06-14) +- [x] Security review (новые API/ввод) — ✅ SECURE, 0 critical (2026-06-14) +- [x] Polish-фиксы по ревью применены: game-блок санитизируется (был латентный XSS), prefers-reduced-motion guard, фикс комментария isUnlocked. Тесты 45/45 затронутых, lint 0. +- [x] `npm test` без новых регрессий (8 = baseline: 3 auth + 5 jsdom) +- [x] `npm run lint:routes` baseline 0 +- [ ] Merged to `feature/sim-builder` (ожидает одобрения пользователя) + +### Deferred / Backlog (не блокеры — из финального ревью) +- `QuantikLevels.ensureCustom` — N+1 `customSimGet` на загрузку /quantik; при росте числа авторённых уровней заменить на bulk-эндпоинт «список game-спек». +- Уровни 3/5/6 (отскок/орбита/манёвр): `fail` — только таймаут, «честная» механика не форсится. Точечно ужесточить `fail`-предикаты (контент-тюнинг). +- `graph-exp-11` капстоун узковат (~36–42/625) — при жалобах на сложность чуть расширить gate. +- Три похожих `starSvg`/`_starIcon` в разных модулях — консолидация не стоит связывания движка с игрой (оставлено). +- Клиентские очки прогресса фальсифицируемы (ожидаемо для single-player). ⚠️ Если когда-нибудь вернут Ф6-лидерборд — валидировать на сервере (replay/подписанные токены). diff --git a/plans/quantik-game/phase-6-leaderboard-live.md b/plans/quantik-game/phase-6-leaderboard-live.md index bd0f66d..88b6ec7 100644 --- a/plans/quantik-game/phase-6-leaderboard-live.md +++ b/plans/quantik-game/phase-6-leaderboard-live.md @@ -1,6 +1,9 @@ # Phase 6: Класс-лидерборд / живая гонка (classroom SSE) -**Status:** ⬜ Not Started +> **REMOVED (Amendment 1, 2026-06-14)** — фаза не реализуется по решению пользователя. +> Архивный subplan. `game_progress.level_id` (TEXT) уже готов под лидерборд, если фичу вернут. + +**Status:** ❌ Removed **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** fullstack