- TypeScript 82.6%
- JavaScript 16.2%
- CSS 0.9%
- HTML 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .idea | ||
| docker | ||
| docs | ||
| src | ||
| tools | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| editor.html | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
SUPER MEW — прототип
Прецизионный платформер в духе Super Meat Boy на Phaser 4 + TypeScript + Vite. Вся графика и звук генерируются процедурно во время выполнения — внешних файлов-ассетов нет.
Требования
- Node.js 20.19+ или 22.12+ (минимум для Vite 7);
- npm 10+.
Проверено на Node v24.16.0 и npm 11.13.0.
Команды (Windows PowerShell)
В PowerShell всегда используйте
npm.cmd, а неnpm:.ps1-shim блокируется политикой выполнения скриптов.
npm.cmd install # установка зависимостей
npm.cmd run dev # dev-сервер: игра -> http://localhost:5173, редактор -> /editor.html
npm.cmd run dev:editor # dev-сервер и сразу открыть редактор (/editor.html)
npm.cmd run build # tsc --noEmit && vite build -> dist/ (index.html + editor.html)
npm.cmd run preview # предпросмотр собранной версии
npm.cmd run preview:editor # предпросмотр собранного редактора (/editor.html)
Smoke-тест
npm.cmd run smoke собирает проект (tsc --noEmit && vite build) и прогоняет игру
в headless-браузере через Chrome DevTools Protocol (CDP) — без единой npm-зависимости
(только встроенные node:http/node:fs и глобальные fetch/WebSocket из Node 22+).
npm.cmd run smoke:fast запускает тот же тест против уже собранного dist/ без
пересборки (~2–3 с) — удобно для быстрых итераций после npm.cmd run build.
Скриншоты по умолчанию отключены: обычный прогон не создаёт tools/.smoke/ и
печатает строку Скриншоты: отключены. Включить можно флагом
node tools/smoke.mjs --screenshots (после npm.cmd run build; либо
npm.cmd run smoke:fast -- --screenshots) или переменной окружения
SMOKE_SCREENSHOTS=1 (в PowerShell: $env:SMOKE_SCREENSHOTS=1; npm.cmd run smoke).
Когда скриншоты включены, PNG (smoke.png, level-N.png, dpr2.png) сохраняются в
tools/.smoke/; этот каталог игнорируется git (см. .gitignore).
Что проверяется в реальном браузере:
- нет ли ошибок и исключений в консоли;
- на старте активна
MenuScene(выбор уровня):getMenuState()возвращает число уровней, совпадающее сLEVELS, и текущий выбранный индекс; - после старта уровня из меню
GameSceneдействительно активна; - персонаж двигается вправо и прыгает (по измеренной дельте координат);
- прогон по всем уровням через
scene.startLevel(i): уровень активируется, размеры сетки больше нуля, ровно одна цель; механики проверяются по метаданным уровня (пилы/рельсы изobjects,Tпружины,Bблоки) — созданы там, где объявлены, и отсутствуют там, где нет; для каждого уровня сохраняется скриншотtools/.smoke/level-N.png(только при включённых скриншотах); - механики проверяются в рантайме: пружина реально подбрасывает игрока вверх
(скорость по
Y), разрушаемый блок исчезает от касания, пила по рельсу двигается; - HUD действительно на экране: сцена
HudSceneактивна, а все её тексты (уровень, попытки, время, лучшее) целиком лежат внутри буфера[0, width] x [0, height]; - рендер-схема проверяется напрямую: DPR=1 (
1920x1080, zoom 3) и DPR=2 (1280x720CSS → буфер2560x1440, zoom 4, HUD целиком в буфере), плюс скриншотtools/.smoke/dpr2.png(только при включённых скриншотах); в отчёте печатаются строкиDPR=1/DPR=2, а замер FPS идёт 1:1; Escиз игры открывает паузу: активнаPauseScene,GameSceneна паузе;Enter(«Продолжить») возвращает в игру, а↓ ×2+Enter(«В меню») выводит вMenuScene;- экран результата:
finishLevel()поднимаетResultSceneс временем/попытками, а «Дальше» (Enter) запускает следующий уровень; - таблица лидеров (#27): завершённые забеги пишутся в
localStorage(supermew.runs.<имя>), а оверлейLeaderboardSceneотдаёт состояние и JSON-экспорт (exportLeaderboard); - отдельное приложение-редактор (
/editor.html) открывается,getEditorState()возвращает загруженный встроенный уровень с валидной структурой; правка клетки и сохранение переопределения вlocalStorageзатем отражаются в игре после перезагрузки (метаданные уровня получают ожидаемую механику), а смена размера пилы через свойства объекта доходит доGameScene(sawDiameter = size × TILE); legacy-правка{rows}мигрирует с сохранением объектов, число уровней в меню совпадает; - общий скриншот
tools/.smoke/smoke.png(только при включённых скриншотах); - в конце печатается таблица таймингов фаз (запуск браузера, загрузка, готовность
сцены, геймплей, прогон уровней, скриншот, завершение); при отключённых скриншотах
фаза
скриншотзанимает ~0 мс.
Нужен локальный Chromium (Chrome или Edge); при необходимости путь задаётся
переменной окружения CHROME_PATH. Exit code 0 — все уровни активны и ошибок нет.
Управление
| Действие | Клавиши |
|---|---|
| Движение | ← → или A D |
| Прыжок (высота зависит от удержания) | Space / W / ↑ |
| Прилипание к стене / медленное сползание | автоматически при падении у стены (ввод в стену не нужен) |
| Прыжок от стены (wall-jump) | прыжок у стены; короткий лок позволяет зацепиться за ту же стену и подниматься |
| Пауза (меню «Продолжить/Заново/В меню») | Esc (в паузе: ↑↓/W S + Enter, Esc — продолжить) |
| Рестарт уровня | R |
| Музыка вкл/выкл | M |
| Таблица лидеров (в меню) | L (оверлей: Tab/←→ — уровень, E — экспорт, Esc/Enter — назад) |
Редактор — отдельное приложение, см. раздел «Редактор уровней»; в меню игры
дополнительно появляются сохранённые свои уровни (★).
Меню и прогресс
При запуске BootScene генерирует текстуры и сразу открывает MenuScene — выбор уровня:
вверх/вниз (↑ ↓ или W S) двигают выделение, Enter (или Space) запускает
выбранный уровень, а цифры 1–9 прыгают сразу к уровню с этим номером (если он есть).
Во время игры Esc открывает паузу («Продолжить/Заново/В меню»). Пройденный уровень
показывает экран результата (время/лучшее/попытки, секрет, «Дальше») с автопереходом на следующий,
после последнего — возврат в меню. Встроенных уровней — 40: 1 «Первый шаг» (40×22) — обучение, 2–19 — прогрессивная лесная кампания (палитра forest: платформы и mover, шипы, пружины, пилы и рельсы, исчезающие блоки и прессы, лёд и прыгучие стены, конвейеры, порталы, лестницы, ключи и замки, ящики, ветер и лазеры, финал «Сердце леса»), 20 «Колосс» — боссфайт, 21–40 — полная ледяная кампания (палитра ice: скользкий и тонкий лёд, пружины, конвейеры, кнопки и двери, порталы, ключи и замки, орбиты, ветер-порывы и лазеры, чейзер-лазер, секреты) с финальным боссом 40 «Морозный страж» (kind 'frost').
Прогресс сохраняется в localStorage: пройденные уровни отмечаются галочкой ✓,
а лучшее время каждого уровня (supermew.best.<имя уровня>) показывается в строке уровня
и суммируется в общий результат. Чтение и запись — в src/progress.ts. Список уровней
меню строится из resolveLevels() (встроенные с правками редактора + свои).
Завершённые забеги встроенных уровней складываются в локальную таблицу лидеров
(supermew.runs.<имя уровня>, хранятся LEADERBOARD_MAX лучших по времени). В меню клавиша L
открывает оверлей таблицы: ранг/время/смерти/дата, переключение уровней Tab/←→,
E — экспорт JSON (в буфер обмена, иначе файл leaderboard.json), Esc/Enter — назад.
Редактор уровней (отдельное приложение)
Редактор — отдельное SPA editor.html (исходники в src/editor/), в духе Paint:
UI на DOM/CSS, карта рисуется на 2D-canvas. Phaser в рантайме редактора не участвует.
Dev: http://localhost:5173/editor.html (или npm.cmd run dev:editor); сборка:
dist/editor.html. Общий origin с игрой → общий localStorage, поэтому редактор меняет
текущие, встроенные уровни, и правка сразу видна игре.
Раскладка: сверху CSS-меню (новый, сохранить, сбросить, импорт, экспорт, .ts, отмена,
вернуть, тест, справка); слева список уровней (встроенные, правки помечены ●, свои
★); по центру холст с зумом и панорамой; справа свойства (имя, размер, выделенный
объект, проверка); снизу панель инструментов и палитра из 37 символов в 4 группах («Основные», «Тайлы», «Объекты», «Механики»).
Инструменты (V/B/E/H) и палитра из 37 элементов в 4 группах:
| Инструмент | Клавиша | Что делает |
|---|---|---|
| Выделение | V |
выбирает объекты и клетки тайлов (клик или рамка; Shift — добавить к выделению); свойства — в инспекторе |
| Кисть | B |
рисует символом; для X/R/H/V ставит объект (протяжка задаёт размер/длину) |
| Ластик | E |
очищает клетки |
| Панорама | H / пробел / СКМ |
тащит холст |
| Управление | Клавиши / мышь |
|---|---|
| Стереть клетку / удалить объект | ПКМ (протяжка стирает тайлы) |
| Зум | Ctrl + колесо, + / −, кнопки в углу холста |
| Вписать в экран | Ctrl+0 или кнопка ⤢ |
| Панорама | ←↑↓→ (Shift — быстрее), колесо, СКМ |
| Сетка вкл/выкл | G |
| Выбор символа палитры | 1–9, 0, [ / ] |
| Следующий/предыдущий уровень | Tab / Shift+Tab |
| Дубликат выделения (при переносе) | Ctrl / Cmd + протяжка центра |
| Сохранить правку | Ctrl+S |
| Отменить / вернуть | Ctrl+Z / Ctrl+Y |
| Новый уровень (имя и размер) | N |
| Переименовать | K (фокус на поле имени) |
| Тест без сохранения | P |
| Тест с сохранением правки | Shift+P |
| Удалить объект / сбросить к встроенному | Del |
| Очистить нутро / вернуть рамку | кнопки в свойствах |
Импорт/экспорт JSON, экспорт .ts |
кнопки меню (модалка с текстом) |
Символы тайлов и правила — как в контракте уровня (см. ниже); пилы/рельсы/платформы —
это объекты objects[] (пила X, рельс R, платформа H/V), у них есть
собственные свойства. Спавн S и цель G уникальны — постановка нового стирает прежний.
Инспектор показывает размер, тип уровня (встроенный / правка / свой), число объектов,
результат проверки (format.ts; невалидный уровень помечается ⚠ — живой чип у холста и
бейдж в списке уровней) и — при выборе объекта инструментом V — его свойства
(тип, позиция, диаметр пилы в тайлах, длина рельса/платформы, ось) с кнопкой удаления.
Выделенные рамкой клетки тайлов подсвечиваются и переносятся/удаляются вместе с объектами.
Сохранение (Ctrl+S) пишет переопределение по имени уровня в localStorage
(supermew.leveledits, см. src/levels/edits.ts). Игра собирает итоговый список
resolveLevels() — встроенные с правками плюс свои — и сразу их использует. Del
удаляет правку и возвращает встроенную версию, а экспорт .ts даёт готовый модуль для
src/levels/, если правку нужно закрепить в исходниках. Кнопка «Тест» (P) пишет текущий
уровень во временный канал (supermew.preview) и открывает игру по адресу /?preview=1 —
стартует незакоммиченная правка, минуя сохранение и меню; Shift+P сохраняет правку и
открывает /?play=<имя> (тоже сразу, минуя меню).
Проверка «Теста» в смоуке: приложение /editor.html поднимается, window.__supermewEditor
отдаёт состояние, UI содержит панель инструментов/палитру/холст; правка клетки и сохранение
применяются в игре после перезагрузки.
Механики
- Coyote time — прыжок ещё возможен короткое время после схода с края;
- Jump buffer — нажатие прыжка чуть раньше приземления не теряется;
- Variable jump — высота прыжка зависит от того, как долго удерживается клавиша;
- Wall slide + wall jump — автоприлипание к стене и медленное сползание (жать в стену не нужно, ввод от стены отпускает её), а короткий input-lockout после отталкивания позволяет зацепиться за ту же стену и подниматься по ней;
- Вращающиеся пилы — статичные диски-ловушки: объект
sawзадаёт центр и диаметр в тайлах (size); касание убивает, а вращение — чисто визуальный эффект; - Пилы по рельсу — объект
railзадаёт рельс (lengthклеток) и диаметр пилы (size), по рельсу пила ездит туда-обратно (касание убивает); - Пружины — тайл
Tподбрасывает игрока вверх; импульс не режется variable jump и рассчитан на высокие платформы, недостижимые обычным прыжком; - Разрушающиеся блоки — тайл
Bтвёрдый: по нему можно пройтись, но спустя короткую задержку после касания он рассыпается (с предупреждающим оттенком) и восстанавливается при респауне; - Исчезающие блоки — тайл
Pмигает по циклу (периодVANISH_PERIOD_MS): твёрдый только в активной фазе, вне неё становится призрачным и не держит игрока; - Пресс-давилка — объект
pressбьёт вниз или вбок по таймеру (покой → удар → пауза → возврат); опасен на всём ходу, хитбокс — динамическое тело; фазаphaseрассинхронивает несколько прессов; - Чекпоинты — тайл
Fзапоминает точку респавна: после смерти игрок возвращается к последнему взятому флагу (активный флаг подсвечивается зелёным), аR/начало уровня сбрасывают чекпоинт; - Секретные зоны — объект
secret(?) ставит зону-собирашку: вход засчитывает секрет, а при входе не позже окнаtime(мс, по умолчаниюSECRET_REWARD_MS) — временную награду; находка отмечается★в меню, награда —★!, а вlocalStorage(supermew.secret.<имя уровня>) хранится лучшее время сбора; - Мгновенный респаун — смерть с hit-stop (короткая заморозка кадра + тряска и вспышка
камеры), счётчик попыток и почти мгновенный возврат к точке спавна (
DEATH_DELAY_MS); - Персистентные декали — пятна крови от смертей остаются на уровне между попытками
(слой
RenderTexture, доDECAL_MAXштампов) и сбрасываются при перезагрузке уровня; - Глобальный time-scale — единый масштаб логики и физики (
GameScene.timeScale), на котором построены hit-stop и bullet-time смерти; - Экран результата — время/лучшее/попытки и «Дальше» с автопереходом на следующий уровень;
- Фоновая музыка — процедурный чиптюн-луп на Web Audio API (без файлов), вкл/выкл по
M; - Таймер уровня и лучшее время — сохраняется в
localStorage(supermew.best.<имя уровня>).
Внутреннее разрешение и HUD
Canvas рендерится в физических пикселях: backing store равен CSS-размер × devicePixelRatio,
а canvas показывается 1:1 (scale.zoom = 1/DPR, Scale.NONE) — без CSS-масштабирования
и nearest-downscale, поэтому картинка остаётся резкой при любом DPR, включая дробные
125 %/150 %. Размер буфера и зум пересчитываются на resize/смене DPR через refit()
в src/main.ts (буфер = round(innerWidth × DPR) × round(innerHeight × DPR)).
Мир (GameScene) рендерится с целочисленным зумом камеры
worldZoomForHeight(bufferHeight) = max(1, round(bufferHeight / 360)) — при буфере
1080px это zoom = 3, при 1440px — zoom = 4. Видимая область мира остаётся 640x360,
но пиксель-арт ложится на сетку физических пикселей без полупикселей. Пилы создаются
как текстуры diameter × zoom и показываются в мировом размере diameter, то есть на
экране попадают в буфер 1:1 (без downscale); текстуры пересоздаются при смене зума
(refreshSawTextures).
HUD вынесен в отдельную сцену src/scenes/HudScene.ts с зумом 1, поэтому его координаты —
физические пиксели буфера, а верстка пересчитывается под текущий целочисленный зум мира.
Крутилки RENDER_SCALE больше нет: итоговое разрешение полностью определяется DPR окна
и высотой буфера.
Структура проекта
index.html точка входа игры, монтирует Phaser в #game
editor.html отдельное приложение-редактор уровней
vite.config.ts конфиг сборки: две страницы (index/editor) + Phaser отдельным чанком
src/
main.ts конфиг Phaser.Game игры, сцены, физика, масштаб; хук __supermew
editor/main.ts контроллер редактора: DOM/CSS-UI, инструменты, выделение/свойства, хоткеи, история, API
editor/render.ts 2D-canvas: сетка, тайлы, объекты, выделение, «призрак», свотчи
editor/editor.css тема редактора (CSS, без внешних зависимостей)
constants.ts все настраиваемые константы (физика, палитра, ключи текстур)
levels/ по одному JSON-уровню (tiles + objects) + реестр/формат/правки
index.ts реестр LEVELS, реэкспорт типов/формата/объектов
types.ts LevelDef/LevelObject и контракт формата
objects.ts геометрия/умолчания объектов (пилы, рельсы, платформы)
format.ts валидация, JSON<->LevelDef, миграция legacy-ASCII, экспорт в .ts
edits.ts правки встроенных/свои уровни в localStorage, resolveLevels()
level-NN-*.ts отдельные встроенные уровни
textures.ts процедурная генерация всех текстур (Graphics -> generateTexture)
sfx.ts процедурный звук + чиптюн-луп на Web Audio API (синглтон sfx)
fx.ts системы частиц: кровь, пыль, прыжковый puff
progress.ts прогресс в localStorage: прохождение, лучшее время, смерти, ghost, таблица лидеров
entities/Player.ts контроллер персонажа (coyote/buffer/wall-jump)
scenes/BootScene.ts создаёт текстуры и запускает MenuScene
scenes/MenuScene.ts выбор уровня (resolveLevels), прогресс
scenes/GameScene.ts сборка уровня (индекс или customLevel), коллизии, смерть/респаун, таймер
scenes/HudScene.ts отдельная сцена HUD с зумом 1 (поверх игрового мира)
scenes/PauseScene.ts меню паузы (зум 1), открывается по Esc, ставит GameScene на паузу
scenes/ResultScene.ts экран результата (зум 1): время/лучшее/попытки, «Дальше», автопереход
scenes/LeaderboardScene.ts таблица лидеров (зум 1): забеги уровня, Tab/←→, экспорт JSON (E)
Настройка
Все игровые параметры собраны в src/constants.ts: гравитация, скорости, ускорения,
сила прыжка и wall-jump, coyote/buffer-тайминги, палитра и ключи текстур. Меняйте значения там —
остальной код импортирует имена из этого файла.
Как добавить уровень
Каждый встроенный уровень — отдельный файл src/levels/level-NN-*.ts, экспортирующий
LevelDef ({ name, tiles, objects, palette? }); реестр src/levels/index.ts собирает их в
LEVELS, а resolveLevels() (см. src/levels/edits.ts) накладывает правки редактора
и добавляет свои уровни. Добавьте новый файл и подключите его в реестре — или нарисуйте
уровень в редакторе и нажмите кнопку «В .ts», чтобы получить готовый модуль для этой папки.
tiles — статичные тайлы (строки одинаковой длины):
| Символ | Значение |
|---|---|
# |
сплошной блок |
= |
односторонняя платформа (проходима снизу, стоит только сверху) |
T |
пружина: подбрасывает игрока вверх при касании |
B |
разрушающийся блок: остаётся твёрдым, пока по нему идут, и рушится с задержкой после касания; восстанавливается после смерти |
i |
тонкий лёд: твёрдая плита, крошится через ICE_BREAK_MS после приземления сверху (в отличие от B — боковое/нижнее касание не срабатывает); трещины нарастают перед обвалом, восстанавливается после смерти |
P |
исчезающий блок: мигает по циклу и твёрдый только в активной «фазе» |
F |
флаг-чекпоинт: промежуточный респавн; порядок — слева-вправо, сверху-вниз; после смерти возвращает на последний взятый флаг |
S |
точка спавна (ровно одна) |
G |
цель-триггер (ровно одна) |
^ v < > |
шипы вверх / вниз / влево / вправо |
|
пусто |
objects — параметризованные сущности:
| Объект | Поля | Значение |
|---|---|---|
saw |
x, y, size |
вращающаяся пила: x/y — центр диска в клетках (может быть дробным), size — диаметр в тайлах (по умолчанию 1) |
rail |
x, y, length, size |
пила по рельсу: рельс от клетки x вправо на length клеток, по нему ездит пила диаметром size тайлов |
mover |
x, y, length, axis |
движущаяся односторонняя платформа длиной length клеток; axis — 'x' или 'y' (ось колебания) |
press |
x, y, length, axis, dir, phase |
пресс-давилка: исходный габарит от x/y на length клеток; axis — ось удара ('y' вниз/вверх, 'x' вбок), dir — 1/-1 направление, phase (0..1) — сдвиг фазы; периодически бьёт и опасен |
secret |
x, y, time? |
секретная зона (объект ?): x/y — клетка; вход засчитывает секрет, time (мс, по умолчанию SECRET_REWARD_MS) — окно от старта уровня для временной награды |
palette — необязательная тема оформления из PALETTES (src/constants.ts):
default, forest, ice, sawmill, hell, void. Задаёт фон и цвета тайлов, платформ и
шипов; отсутствие поля равнозначно default. Выбирается в редакторе («Свойства карты»).
tiles должна быть прямоугольной, замкнутой по периметру # и иметь сплошной пол.
Обновляйте ASCII-комментарий над уровнем при изменении геометрии.
Примечание
Внешних файлов изображений и звуков нет: текстуры рисуются в src/textures.ts,
звуки синтезируются в src/sfx.ts через Web Audio API.