Files
Learn_System/plans/quantik-game/phase-2-map-world-xp.md
T
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

114 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 2: Карта-созвездие + мир физ-уровней + XP/скины (MVP-мир)
**Status:** ✅ Done (reviewed — PASS w/ notes; «Дальше» stale-hasNext 🟡 fixed; committed)
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
## Objective
Превратить одиночный уровень в **играбельный мир**: карта-созвездие из ~5–6 физ-уровней,
разблокировка по звёздам, XP, выбор скина Квантика, нарратор-Квантик (`PetSprite`) на интро/
победе. После этой фазы игра полноценно отгружаема.
## Tasks
- [x] Task 1: Контент — ~5–6 физ-уровней-спек (данные в `levels.js`), нарастающая сложность:
артиллерия → перелёт через стену → отскок (restitution) → пружина/маятник → орбита/гравитация.
Каждый: `goal` + 1–3 звезды + норматив времени (`par_ms`) для 3-й звезды.
- [x] Task 2: Структура «мир/глава»: метаданные уровня (id, title, chapter, order, par_ms, hint).
Карта группирует по главам (созвездиям).
- [x] Task 3: Карта-созвездие `frontend/js/game/map.js` (+ разметка в quantik.html): узлы-уровни
на SVG/canvas-фоне, линии-связи, статус (заблокирован/доступен/пройден + число звёзд).
Разблокировка: уровень открыт, если набрано ≥ threshold звёзд в предыдущих (правило в данных).
- [x] Task 4: XP/уровень игрока: XP = сумма звёзд × коэффициент (+ бонус за par). Хранить в
прогрессе (расширить `game_progress` агрегацией на клиенте ИЛИ доб. поле/таблицу `game_player`).
Полоса XP + «уровень Квантика» в шапке карты.
- [x] Task 5: Скины Квантика: выбор `colorKey` из палитр `PetSprite` (+ позже паттерны). Скин
влияет на цвет glow-точки героя в уровне (param/проп движка) и на `PetSprite` на карте.
Хранить выбор (localStorage сейчас; серверно — опц.). Разблокировка скинов по XP/звёздам.
- [x] Task 6: Нарратор: `PetSprite.render(...)` в интро уровня (краткая формулировка «почини закон…»)
и на экране победы (реакция по числу звёзд: happy/ecstatic). Реюз mood из pet-sprite.js.
- [x] Task 7: Навигация: карта → уровень → результат → возврат на карту с обновлённым статусом/XP.
- [x] 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)` (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).