Files
Maxim Dolgolyov 0f3e12426a @
feat(quantik-game): фаза 2 — карта-созвездие + мир + XP/скины (MVP-мир)

Одиночный уровень → играбельный мир: карта-созвездие из 6 физ-уровней
(2 главы, нарастающая сложность), разблокировка по звёздам, клиентский
XP/уровень игрока, пикер из 8 скинов (тинт героя+нарратора), нарратор
PetSprite на интро/победе (mood по звёздам). Навигация карта→интро→игра→
успех→карта/дальше; кнопка «Дальше» пересчитывает nextPlayable после
дозагрузки прогресса (фикс stale-hasNext). Логика прогресса — чистый
модуль progress-logic.js (unlock/XP/группировка). Только фронт, без
бэкенда: XP агрегируется из game_progress (Ф1). Каждый уровень проверен
на реальном движке (выигрываем + обе звезды достижимы); цепочка
разблокировки доказуемо проходима. npm test 251/8 baseline; lint:routes 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-13 16:24:31 +03:00

11 KiB
Raw Permalink Blame History

Phase 2: Карта-созвездие + мир физ-уровней + XP/скины (MVP-мир)

Status: Done (reviewed — PASS w/ notes; «Дальше» stale-hasNext 🟡 fixed; committed) Parent plan: PLAN.md Domain: fullstack

Objective

Превратить одиночный уровень в играбельный мир: карта-созвездие из ~5–6 физ-уровней, разблокировка по звёздам, XP, выбор скина Квантика, нарратор-Квантик (PetSprite) на интро/ победе. После этой фазы игра полноценно отгружаема.

Tasks

  • Task 1: Контент — ~5–6 физ-уровней-спек (данные в levels.js), нарастающая сложность: артиллерия → перелёт через стену → отскок (restitution) → пружина/маятник → орбита/гравитация. Каждый: goal + 1–3 звезды + норматив времени (par_ms) для 3-й звезды.
  • Task 2: Структура «мир/глава»: метаданные уровня (id, title, chapter, order, par_ms, hint). Карта группирует по главам (созвездиям).
  • Task 3: Карта-созвездие frontend/js/game/map.js (+ разметка в quantik.html): узлы-уровни на SVG/canvas-фоне, линии-связи, статус (заблокирован/доступен/пройден + число звёзд). Разблокировка: уровень открыт, если набрано ≥ threshold звёзд в предыдущих (правило в данных).
  • Task 4: XP/уровень игрока: XP = сумма звёзд × коэффициент (+ бонус за par). Хранить в прогрессе (расширить game_progress агрегацией на клиенте ИЛИ доб. поле/таблицу game_player). Полоса XP + «уровень Квантика» в шапке карты.
  • Task 5: Скины Квантика: выбор colorKey из палитр PetSprite (+ позже паттерны). Скин влияет на цвет glow-точки героя в уровне (param/проп движка) и на PetSprite на карте. Хранить выбор (localStorage сейчас; серверно — опц.). Разблокировка скинов по XP/звёздам.
  • Task 6: Нарратор: PetSprite.render(...) в интро уровня (краткая формулировка «почини закон…») и на экране победы (реакция по числу звёзд: happy/ecstatic). Реюз mood из pet-sprite.js.
  • Task 7: Навигация: карта → уровень → результат → возврат на карту с обновлённым статусом/XP.
  • Task 8: Тесты: разблокировка (логика чистой функцией — юнит-тест), агрегация XP; смоук карты.

Files to Modify/Create

  • frontend/js/game/levels.js — контент мира (расширить).
  • frontend/js/game/map.js — карта-созвездие.
  • frontend/js/game/quantik-game.js — навигация карта↔уровень, XP/скин в шапке.
  • frontend/quantik.html — разметка карты/шапки.
  • (опц.) backend — поле/агрегация игрока, если решим серверно; иначе клиентская агрегация прогресса.
  • тест(ы) разблокировки/XP.

Acceptance Criteria

  • Карта показывает мир, статусы и звёзды; пройденные уровни открывают следующие.
  • XP/уровень Квантика растут; смена скина видна и на карте, и в уровне.
  • Нарратор-Квантик появляется на интро/победе с корректным настроением.
  • Тесты разблокировки/XP зелёные; lint baseline 0; existing тесты не сломаны.

Notes

  • Без эмодзи — звёзды/иконки только inline SVG (.ic).
  • Разблокировку держать данными/чистой функцией (легко тестировать и переносить на сервер).
  • Не плодить серверные таблицы без нужды: прогресс уже в game_progress (Ф1); XP можно агрегировать.

Review Checklist

  • Все задачи; чистая функция разблокировки покрыта тестом; без эмодзи/eval
  • Карта/навигация работают; existing тесты целы; lint baseline 0

Handoff to Next Phase

Архитектура карты (frontend/js/game/map.js)

  • window.QuantikMap.create({ host, headerHost, onPlay(level), getSkin()->key, onSkin(key) }) -> { render(progressMap), destroy() }.
    • render(progressMap) рисует шапку (нарратор + XP-бар + всего звёзд + скин-пикер) в headerHost и созвездия в host. progressMap{ [level_id]: row } (см. QuantikProgress.fromProgressList).
    • Узел созвездия (buildNode) — <button class="qm-node qm-{locked|available|completed}"> с ядром (.qm-node-core), подписью, звёздами/порогом. Позиция в % через layoutNodes(levels) (зигзаг-дуга).
    • Статус узла = QuantikProgress.nodeStatus. Клик по available/completed → onPlay(level).
    • Звёздное небо — SVG <circle class="qm-tw"> (мерцание CSS), линии-связи <line class="qm-link[.on]">.
    • Поэтапное появление: staggerReveal снимает .qm-pre/ставит .qm-in через setTimeout (70 мс шаг).

Как добавить главу (созвездие)

  • В levels.js: дать новым уровням chapter:'<key>' + добавить запись в CHAPTERS ({ key, title, subtitle, accent }). Карта группирует автоматически (groupByChapter сохраняет порядок появления глав). Узлы внутри главы сортируются по order. Никаких правок map.js не нужно.
  • Фаза 3 (граф-уровни) = НОВАЯ глава (напр. chapter:'functions'): добавить уровни-спеки с objects:[{type:'plot',...}] + goal.when по форме функции; unlockStars гейтит её за Динамику. Узел рисуется тем же buildNode (тип спеки карте безразличен — она читает только метаданные).

Модуль логики прогресса (frontend/js/game/progress-logic.js, window.QuantikProgress)

Чистые функции (без DOM/сети/eval) — переносимы на сервер, покрыты тестом:

  • fromProgressList(list) → карта {level_id: row} из ответа /api/game/progress.
  • starsFor(id, map) / isCompleted(id, map) / totalStars(levels, map).
  • isUnlocked(level, map, levels) — уровень открыт, если Σ звёзд во ВСЕХ уровнях с меньшим orderlevel.unlockStars (порог в данных уровня). unlockStars:0 (или нет) → всегда открыт.
  • nodeStatus / starsToUnlock — для карты.
  • computeXp(levels, map) = Σ(звёзды·STAR_XP=100 + COMPLETE_XP=40 за пройденный).
  • playerLevel(xp){ level, xp, xpInto, xpForNext, progress01 }. Шкала: xpForLevel(L)=240·(L-1)L/2.
  • groupByChapter(levels)[{ chapter, levels:[…sorted by order] }].
  • nextPlayable(curId, levels, map) → след. разблокированный уровень (для кнопки «Дальше») или null.

Скины

  • localStorage ключ quantik-skin (экспортирован QuantikGame.SKIN_KEY). Значение = colorKey из PetSprite.PALETTES (валидируется при чтении, иначе fallback 'cyan').
  • QuantikGame.getSkin()/setSkin(key)/skinColor(key). Тинт героя — tintHeroSpec(spec, key): глубокая копия спеки (JSON), переписывает color/glowColor/trailColor объекта с id:'ball'. Гейты скинов — массив SKIN_GATES в map.js (needStars/needXp). 8 скинов.

Нарратор

  • PetSprite.render(level, mood, [], colorKey, 0, 'none') (DOM SVG-строка). Вызовы:
    • Карта-шапка: QuantikMap.renderHeader (mood по уровню игрока: ecstatic≥5 / happy≥2 / neutral).
    • Интро уровня: QuantikGame.buildIntro(level, skin) (mood happy).
    • Экран успеха: QuantikGame.buildSuccessOverlay(state, {skin, hasNext}) — mood ecstatic, если все звёзды (got≥total и total≥2), иначе happy.

Навигация (inline-bootstrap в quantik.html)

  • Два вида: #qg-map-view (карта) и #qg-level-view (#qg-stage под движок). Переключение классом .show. showMap() перезагружает прогресс (LS.gameProgressList) → map.render. openLevel→интро→launchLevel→onGoal→успех→onNext(nextPlayable)|onMap. При смене уровня ВСЕГДА destroyLevel() (= inst.destroy() + очистка #qg-stage) до нового mount (гоча Ф1).
  • Deep-link ?level=<id> открывает уровень, если он разблокирован; иначе карта.

Решения/гочи (для ревью и Ф3+)

  • XP/прогресс игрока — чисто клиентская агрегация из game_progress (Ф1). Новых таблиц/роутов НЕТ → lint:routes baseline 0 не тронут, бэкенд-тесты не изменились (259, 251 pass / 8 baseline fail).
  • Уровни 3/5/6 имеют «лёгкий» выигрышный путь, попутно дающий обе звезды; «честная» механика (отскок/орбита/колодец) присутствует, но не единственно-возможна — НЕ блокер MVP (см. winnability).
  • На сервер агрегацию XP перенести легко: те же чистые функции в progress-logic.js (без DOM).