diff --git a/CLAUDE.md b/CLAUDE.md index ed6c6e9..1e919b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,10 @@ npx vitest run packages/engine/src/map/__tests__/pathfinding.test.ts # один файл тестов npm run typecheck # tsc --noEmit для обоих пакетов npm run art # перегенерация пиксель-арта из tools/pixelart +npm run maps # перегенерация карт-файлов (tools/maps + encodeMap) node tools/smoke.mjs # смоук-тест в реальном Chromium (скриншот + консоль) +node tools/smoke-ponds.mjs # смоук перехода луга -> Серые пруды +node tools/smoke-quest.mjs # смоук диалога с Ирвином и сумки/журнала ``` Dev-сервер по умолчанию на http://localhost:5173; для отдельного порта: `npm run dev -- --port 5199`. @@ -39,6 +42,7 @@ ### Где что лежит - `docs/engine/` — **документация движка** (по-русски): `README.md` (архитектура и принципы), `getting-started.md`, `core.md`, `render.md`, `input.md`, `maps.md`, `ui-and-dialogue.md`, `assets-audio-save.md`, `art-pipeline.md`, `recipes.md`. При изменении API движка обновляй соответствующий файл. +- `docs/demo.md` — **внутренняя проектная дока среза**: матрица «подсистема движка → где показана в игре» + статус, боевая модель, архитектура, дорожная карта. - `docs/world.md` — библия мира (сеттинг, локации, персонажи, сюжет). **Любой новый контент сверять с ней.** - `docs/art-style.md` — арт-библия: палитра (32 цвета), размеры спрайтов, правила стиля, чеклист ассетов. - `packages/engine/src/` — модули движка: `core/` (Engine, GameLoop, Tween, GameState, StateMachine, Settings, EventBus), `scene/` (SceneManager + fade-переходы), `render/` (Renderer, Camera, IsoDepthLayer, Particles, scale), `input/` (действия, геймпад, VirtualJoystick), `map/` (изометрия, A*, mapFormat, Tiled-импорт), `ui/` (PixelText/VT323, Panel, Button, MenuList, DialogueBox), `dialogue/` (DialogueRunner — view-агностик), `audio/` (шины master/music/sfx), `assets/` (AssetLoader + атласы), `save/`, `math/` (iso, rng, easing). @@ -68,6 +72,9 @@ - **Fade-оверлей переходов должен быть поверх UI**: `SceneManager` поднимает его наверх `uiRoot` в `begin()` — сцены добавляют свои view позже, и «наивный» оверлей из `Engine.init()` оказывается под ними (карты просвечивают сквозь меню). - **TweenManager.tick учитывает пересечение границы `delay`**: часть dt, приходящаяся на delay, вычитается из движения — не упрощай тик до последовательных прибавлений. - **Headless-скриншоты с `--virtual-time-budget` зависают на загрузке картинок** (колбэки декодинга не срабатывают) — для визуальных проверок использовать `tools/smoke.mjs` (puppeteer-core + системный Chromium, реальное время). +- **В headless Chromium `ctx.resume()` без флага `--autoplay-policy=no-user-gesture-required` не резолвится** — смоук-скрипты обязаны передавать этот флаг, а навигация не должна ждать `.then` у `audio.play`. +- **Одна клавиша маппится на несколько действий** (`bindActions` дополняет, а не перезаписывает): Space = `advance` в диалоге и `attack` вне его — не развести обратно нельзя, гардится уровнем сцены. +- **Координаты указателя и worldRoot — виртуальные пиксели** (480×270), экранные CSS-пиксели — это виртуальные × масштаб: `экран = мир + worldRoot.position` (перевод в CSS — домножением на scale). Не смешивай пространства в кликах по миру. - Мир/арт сверять с `docs/world.md` и `docs/art-style.md`: тёплые цвета — только жизнь; палитра — только из `tools/pixelart/palette.mjs`. ## Рабочие привычки diff --git a/README.md b/README.md index 1813c87..d02d381 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,26 @@ ```bash npm install npm run dev # http://localhost:5173 -npm test # 81 юнит-тест движка +npm test # 158 юнит-тестов (движок + игра) npm run build # прод-сборка -node tools/pixelart/gen.mjs # перегенерация пиксель-арта +npm run art # перегенерация пиксель-арта +npm run maps # перегенерация карт-файлов (.map) ``` ## Структура ``` docs/engine # документация движка: архитектура, все подсистемы, рецепты +docs/demo.md # проектная дока среза: матрица «подсистема → где показана» docs/world.md # библия мира: сеттинг, локации, персонажи, сюжет docs/art-style.md # арт-библия: палитра, размеры, стиль, чеклист ассетов packages/engine # движок: цикл, сцены, ввод, камера, изометрия, A*, сейвы, # диалоги, твины, GameState, UI-кит, частицы, аудио, карты -apps/game # RPG: сцены, локации, NPC, диалоги, сюжет -apps/game/assets # пиксель-арт (генерируется tools/pixelart) +apps/game # RPG: сцены, локации, NPC, диалоги, бой, сюжет +apps/game/assets # пиксель-арт и карты (tools/pixelart, tools/maps) tools/pixelart # генератор арта: PNG из кода, палитра в одном месте +tools/audio # генератор WAV: звуки и музыкальные петли из кода +tools/maps # генерация карт-файлов через encodeMap (+тесты формата) ``` Правила: игра использует только публичный API `@rpg/engine`; движок ничего не знает о жанре игры; тёплый цвет в арте — только жизнь (цветы, бронза, огонь). @@ -55,4 +59,7 @@ и цвет; против него работает только резонанс. Звонари прозванивают дороги, лунные колокольчики пьют пепел из земли. Полная библия — в `docs/world.md`. -Первый акт: выжженные луга, Серые пруды, старейшина Ирвин и безгласная торговка Мила. \ No newline at end of file +Первый акт — вертикальный срез-витрина движка: выжженные луга, Серые пруды, старейшина +Ирвин, безгласная торговка Мила, квест «Три цветка» и бой с пепельными сгустками +в реальном времени (удар-конус, резонанс, звук как ресурс). Управление: клик — идти/атаковать, +Space — удар (удержать — резонанс), I — сумка, F3 — отладка, Esc — меню/сейв. \ No newline at end of file diff --git a/docs/demo.md b/docs/demo.md new file mode 100644 index 0000000..d7123d3 --- /dev/null +++ b/docs/demo.md @@ -0,0 +1,86 @@ +# «Пепельные луга» — демо-витрина движка (проектная дока) + +Внутренняя документация вертикального среза. Игра — демо движка `@rpg/engine`: +каждая подсистема задействована в геймплее, недостающие возможности внедряются +в движок жанронезависимо. Пользовательская дока движка — `docs/engine/`, +арт — `docs/art-style.md`, мир — `docs/world.md`. + +## Матрица «подсистема движка → где показана» + +| Подсистема | Где показана в демо | Статус | +|---|---|---| +| Engine (GameLoop, фиксированный шаг 60 Гц) | весь геймплей, `Game.ts` | готово | +| Renderer (480×270, целочисленный масштаб, pixelated) | весь рендер, `main.ts` + `computeScale` | готово | +| Camera (follow, bounds от западного угла, shake) | следование за героем; тряска при резонансе/уроне | готово | +| SceneManager (replace/push/pop, fade) | меню → локация → сумка/сейвы; переходы локаций | готово | +| InputManager (действия, геймпад, мульти-биндинг) | advance/attack/menu/inventory/debug; геймпад-карта | готово | +| VirtualJoystick | тач в локациях (включается при первом касании) | готово | +| IsometricTileMap + A* | карты локаций, движение по клику | готово | +| mapFormat (JSON+RLE) | карты — файлы `assets/maps/*.map`, генерация `npm run maps` | готово | +| Tiled-импорт | не задействован (дока + тест есть) | резерв | +| EventBus | `combat:hurt/attack/playerHit`, `quest:flower` | готово | +| GameState (флаги/вары) | прогресс квеста, hp/kills/motes/deaths; сериализация в сейвы | готово | +| SaveManager (слоты) | autosave (Esc) + 3 слота, `SaveSlotsScene` | готово | +| Settings + onChange | громкости master/music/sfx, `SettingsScene` | готово | +| AudioManager (шины) | музыка локаций с кроссфейдом, sfx боя/шагов/UI | готово | +| Tween + easing | хит-флэш, кольца резонанса, всплывающие тосты | готово | +| Particles (атмосфера + burst) | пепел лугов, туман прудов, удары/смерти сгустков | готово | +| StateMachine | `EnemyBrain` — ИИ сгустков (dormant/rise/chase/…/sleep/dead) | готово | +| ECS (World) | боевые сущности: враги и снаряды на `Entity` | готово | +| Panel/Button/MenuList | меню, настройки, сейвы, сумка | готово | +| DialogueRunner + DialogueBox | графы `data/dialogues.ts`, ветки квеста | готово | +| DebugOverlay | F3: fps/тайл/позиция/hp | готово | +| math/shapes (cone/circle) | конус удара, круг цели, кольца | готово | +| Cooldown | кулдауны удара/резонанса/атак врагов | готово | +| math/rng (mulberry32) | генерация карт и частиц, детерминированный shake | готово | + +## Сюжетная рамка (акт 1, по docs/world.md) + +1. Выжженные луга (хаб): Ирвин даёт квест «Три цветка» (флаг `quest_bells_taken`). +2. Мила предупреждает о накате у прудов, даёт вощёное полотно (`got_cloth`). +3. Серые пруды (опасная зона): туман, сгустки, сбор колокольчиков кликом (вар `flowers`). +4. Возврат: сдача Ирвину (`elder_hand_in`) — посадка цветов у тропы, тайлы зеленеют. +5. Крючок: Мила — «Слышишь гул снизу? Это разъезд. Это Машина дышит» (`trader_after`). + +## Боевая модель (реальное время) + +- Герой: HP 5 (сердечки), удар-конус по facing (34 px, 55°, урон 2, кд 0.45 с), + резонанс удержанием ≥0.6 с (импульс r=96, усыпляет на 4 с, кд 3 с), + неуязвимость 0.8 с после урона, отброс 24 px. +- Враги «пепельные сгустки»: ползун (HP 3, стая), плевун (HP 2, дистанция + плевок), + наст (HP 8, замах → удар по площади). Звук — ресурс: удар излучает гул r=150 + и будит спящих сгустков. +- Ввод: клик по врагу — авто-подход и удар; Space тап/удержание; геймпад X. + +## Архитектура среза + +- `packages/engine` — жанронезависимый движок, публичный API только `src/index.ts`. +- `apps/game/src/`: + - `data/` — контент: `locations.ts` (определения локаций), `map.ts` (генераторы), + `dialogues.ts` (графы), `npcs.ts`, `enemies.ts`, `quests.ts` (журнал/сумка); + - `scenes/` — BootScene → MenuScene (+ SettingsScene, SaveSlotsScene) → + LocationScene (+ InventoryScene); + - `systems/` — PlayerController (путь + прямое движение), DialogueSystem, + `combat/` (CombatWorld, EnemyBrain, PlayerCombat, CombatViews, HealthBar, stats). +- Инструменты: `tools/pixelart` (PNG-атласы), `tools/audio` (WAV-генератор), + `tools/maps` (карты-файлы), `tools/smoke*.mjs` (смоук в реальном Chromium). + +## Новое в движке за срез + +- `render/shake.ts` — `Shake` + `Camera.addShake` (детерминированный rng). +- `math/shapes.ts` — `inCircle`, `inCone`, `angleBetween`, `nearest`. +- `core/Cooldown.ts` — `trigger/update/ready/progress`. +- `render/particleSim.ts` — чистая интеграция частиц; `Particles.burst/oneShot`. +- `anim/flash.ts` + `render/spriteFx.ts` — `TintFlash`/`flashSprite`. +- `input/InputManager` — `isActionJustReleased`, `wasKeyReleased`, `pointer.downTicks`, + мульти-биндинг (одна клавиша → несколько действий). +- `core/StateMachine.transitionAny` — переход из любого состояния. +- `map/IsometricTileMap.setTile` — изменение тайла с перерисовкой ячейки. + +## Дорожная карта (что осталось за срезом) + +- Акт 1 до конца: посадка меняет пейзаж шире (поляна), финальная кат-сцена с Tween-камерой. +- Экономика Милы: торговля (соль/моты), мот-песок как валюта. +- Звонная книга Ирвина: журнал заданий расширяется за пределы одного квеста. +- Tiled-импорт: альтернативный путь производства карт (для больших локаций). +- Спектакль звонарей: мини-сцена с StateMachine-хореографией (витрина StateMachine вне боя). \ No newline at end of file diff --git a/docs/engine/input.md b/docs/engine/input.md index e2e41ad..aac3a96 100644 --- a/docs/engine/input.md +++ b/docs/engine/input.md @@ -20,6 +20,9 @@ engine.input.isActionJustReleased('attack'); // отпущено в этом тике (клавиши+геймпад) ``` +Одна клавиша может маппиться на **несколько действий** (например Space = `advance` в +диалоге и `attack` вне его) — повторные `bindActions` не перезаписывают, а дополняют. + Отпускание и удержание нужны для заряжаемых действий (замах, резонанс). Для точных кодов есть `wasKeyPressed(code)` / `wasKeyReleased(code)`; у указателя — `pointer.downTicks` (сколько тиков удержана кнопка, 0 — только нажата). diff --git a/docs/engine/maps.md b/docs/engine/maps.md index 5753fa7..f633c1c 100644 --- a/docs/engine/maps.md +++ b/docs/engine/maps.md @@ -40,6 +40,7 @@ map.isWalkable(x, y); // Grid для A* map.screenSize; // { width, height } для границ камеры +map.setTile(x, y, id); // изменить тайл (сбор предметов, посадка) — данные + перерисовка ячейки ``` ## A* (pathfinding) diff --git a/docs/engine/ui-and-dialogue.md b/docs/engine/ui-and-dialogue.md index 3ca0e11..deaa9c3 100644 --- a/docs/engine/ui-and-dialogue.md +++ b/docs/engine/ui-and-dialogue.md @@ -152,4 +152,20 @@ uiRoot.addChild(box.view); box.show({ speaker: 'Ирвин', text: 'Привет.' }); box.hide(); -``` \ No newline at end of file +``` + +## DebugOverlay + +Отладочная плашка (fps + произвольные строки). Добавьте `view` в `uiRoot` поверх всего +и вызывайте `update(dt)` каждый тик; видимость переключайте через `view.visible` +(в демо — по F3): + +```ts +import { DebugOverlay } from '@rpg/engine'; + +const debug = new DebugOverlay(false); // скрыт, пока не понадобится +uiRoot.addChild(debug.view); + +debug.setLines(['tile 14,14', 'hp 5']); // строки статуса (fps подставляется сам) +debug.update(dt); // каждый тик +``` diff --git a/tools/smoke-ponds.mjs b/tools/smoke-ponds.mjs index 827f71b..8077667 100644 --- a/tools/smoke-ponds.mjs +++ b/tools/smoke-ponds.mjs @@ -57,7 +57,7 @@ }; }).then((p) => page.mouse.click(p.x, p.y)); } -for (let i = 0; i < 18; i++) { +for (let i = 0; i < 20; i++) { const near = await page.evaluate(() => { const sc = window.__game.scenes.current; return sc && Math.max(sc.player.currentTile().x, sc.player.currentTile().y) <= 5; @@ -79,6 +79,9 @@ await new Promise((r) => setTimeout(r, 700)); } +// Даём fade-переходу (0.4 с) и enter() новой сцены завершиться до скриншота. +await new Promise((r) => setTimeout(r, 1500)); + await page.screenshot({ path: shot }); console.log(`Скриншот: ${shot}`); await browser.close(); \ No newline at end of file