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> @
11 KiB
11 KiB
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)— уровень открыт, если Σ звёзд во ВСЕХ уровнях с меньшимorder≥level.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)(moodhappy). - Экран успеха:
QuantikGame.buildSuccessOverlay(state, {skin, hasNext})— moodecstatic, если все звёзды (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).