From 3bb46e2f6b843ae1d5c7b1131c9dada702829c2f Mon Sep 17 00:00:00 2001 From: Yuriy Mayatnikov Date: Thu, 18 Jun 2026 15:16:57 +0300 Subject: [PATCH] =?UTF-8?q?feat(render):=20=D0=BF=D1=81=D0=B5=D0=B2=D0=B4?= =?UTF-8?q?=D0=BE-3D,=20=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0=20=D0=B2?= =?UTF-8?q?=D0=B8=D0=B4=D0=B8=D0=BC=D1=8B=D0=B5=20=D0=B4=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=B8,=20=D0=B0=D1=81=D1=81=D0=B5=D1=82=D1=8B+=D1=8D=D1=84?= =?UTF-8?q?=D1=84=D0=B5=D0=BA=D1=82=D1=8B,=20=D1=80=D1=83=D1=81=D0=B8?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Рендер переведён в псевдо-3D: наклонная PerspectiveCamera, пол лежит плашмя, персонажи/враги — вертикальные спрайты-биллборды, стены с высотой. Починен баг: пол создавался без поворота и стоял вертикально → «вывернутая» перспектива. - Двери видны всегда: закрыты (засов) во время боя, открытый проём после зачистки. - render/assets.ts: процедурные текстуры (спрайты персонажей/врагов, пол, стены, двери, снаряд, тень) + эффекты оружия (вспышка из дула, искры, облачко гибели). - Светлее палитра — исправлено «тёмное на тёмном». - Игра переименована в «Биндим Фигняшку»; полная русификация UI (ДАЛЬНИЙ/БЛИЖНИЙ, ИГРА ОКОНЧЕНА, ПОБЕДА); клавиши-подсказки оставлены латиницей. - Ввод по event.code → WASD/Q/R работают в любой раскладке (вкл. русскую). - theme.ts урезан до реально используемых тинтов (bg/swing/flash). - docs/ASSET_BRIEF.md — бриф и промпт на полную художку; доки синхронизированы. - Ядро (src/core) не тронуто — раунд чисто render/UI. 27 тестов + типы зелёные. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 12 +- README.md | 20 +- docs/ARCHITECTURE.md | 55 +++-- docs/ASSET_BRIEF.md | 105 +++++++++ docs/HOWTO.md | 26 ++- index.html | 4 +- src/input/KeyboardController.ts | 41 ++-- src/main.ts | 4 +- src/render/HudOverlay.ts | 6 +- src/render/ThreeRenderer.ts | 371 ++++++++++++++++++++++---------- src/render/assets.ts | 230 ++++++++++++++++++++ src/render/theme.ts | 53 ++--- 12 files changed, 716 insertions(+), 211 deletions(-) create mode 100644 docs/ASSET_BRIEF.md create mode 100644 src/render/assets.ts diff --git a/CLAUDE.md b/CLAUDE.md index a1897b0..4065f82 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ ## Что это Top-down рогалик (в духе Binding of Isaac). Логика — чистый TypeScript -(`src/core/`), рендер — three.js с ортокамерой (`src/render/`), сборка/тесты — Bun. +(`src/core/`), рендер — three.js в псевдо-3D (наклонная `PerspectiveCamera`, `src/render/`), сборка/тесты — Bun. Обзор — [`README.md`](./README.md), детали — [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md), рецепты доработки — [`docs/HOWTO.md`](./docs/HOWTO.md). @@ -64,8 +64,9 @@ bun run check # typecheck + test | форму комнаты/тайлы | `src/core/world/tiles.ts`, `Room.ts` | | коллизии | `src/core/systems/collision.ts` | | расстановку врагов | `src/core/systems/spawner.ts` | -| как рисуется мир | `src/render/ThreeRenderer.ts` | -| цвета/внешний вид/ассеты мира | `src/render/theme.ts` | +| как рисуется мир (псевдо-3D) | `src/render/ThreeRenderer.ts` | +| спрайты/текстуры (графика) | `src/render/assets.ts` (бриф на художку — `docs/ASSET_BRIEF.md`) | +| цвета/тинты/фон | `src/render/theme.ts` | | HUD/миникарту | `src/render/HudOverlay.ts` | | стартовое меню | `src/ui/StartMenu.ts` | | раскладку клавиш | `src/input/KeyboardController.ts` | @@ -80,8 +81,9 @@ bun run check # typecheck + test Если карта пустая — `curRoom` будет `undefined` и всё упадёт на старте. - **`OPP` направлений.** Противоположное к `up` — это `down`, к `left` — `right`. Любая другая раскладка ломает встречные двери и связность карты. -- **Чёрный экран при работающем рендере.** Ортокамера инвертирует Y → нужен - `DoubleSide` на материалах, иначе грани отсекаются. +- **Пол «выворачивает» перспективу.** Пол в псевдо-3D надо класть ПЛАШМЯ + (`flatMesh`, поворот −90° вокруг X). Без поворота он встаёт вертикально и вид + ломается. Спрайты/стены — `DoubleSide` (камера переворачивает Y, иначе грани отсекаются). - **Canvas вылезает за рамки.** Холстам нужен CSS-размер (`width/height:100%`), иначе они показываются в размер HiDPI-буфера. - **Комната без врагов не открывается.** Если в комнате 0 врагов (сокровищница) — diff --git a/README.md b/README.md index c4b6c2a..76487a6 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ -# Dungeon Crawl — рогалик в духе The Binding of Isaac +# Биндим Фигняшку — рогалик в духе The Binding of Isaac Top-down рогалик: процедурный данжен из комнат, два режима боя (дальний/ближний), -враги, босс. Логика на чистом TypeScript, рендер — на **three.js** (ортографическая -камера, картинка плоская 2D-сверху). Сборка — **Bun**. +враги, босс. Логика на чистом TypeScript, рендер — на **three.js** в **псевдо-3D**: +наклонная камера, пол лежит плашмя, персонажи — вертикальные спрайты-биллборды +(как в Isaac). Сборка — **Bun**. Есть **стартовое меню** с выбором уровня: данжен генерируется процедурно каждый забег, но параметризуется набором правил (размер, плотность/сила врагов, HP, seed). -Внешний вид мира вынесен в «тему» — задел под кастомные ассеты (спрайты/текстуры). +Двери видны всегда (закрыты в бою, открыты после зачистки). Графика — процедурные +ассеты в `render/assets.ts`; бриф на полную художку — в `docs/ASSET_BRIEF.md`. > Это рабочая **база для развития**, а не готовая игра. Архитектура специально > сделана так, чтобы её было легко расширять — и человеку, и ИИ-агентам. @@ -44,6 +46,9 @@ bun run check # и то, и другое разом | R | заново (на экране Game Over / Victory) | | Esc | вернуться в меню выбора уровня | +Клавиши привязаны к **физическим** кнопкам (`event.code`), поэтому WASD/Q/R работают +в любой раскладке (в т.ч. русской), независимо от языка ввода. + Чтобы перейти в соседнюю комнату — зачисти текущую (двери откроются) и встань на дверь, нажимая в её сторону. @@ -66,7 +71,7 @@ bun run check # и то, и другое разом ## Стек и устройство - **Bun** — рантайм, бандлер и тест-раннер. -- **three.js** — WebGL-рендер мира через ортокамеру. +- **three.js** — WebGL-рендер мира в псевдо-3D (наклонная `PerspectiveCamera`, биллборд-спрайты). - **TypeScript (strict)** — весь код. - Архитектура: **логика игры полностью отделена от рендера**. Ядро (`src/core/`) не знает ни про DOM, ни про three.js, поэтому его легко тестировать и при @@ -94,8 +99,9 @@ src/ │ └── KeyboardController.ts клавиатура → InputState ├── render/ ── РЕНДЕР (читает состояние, рисует) ── │ ├── Renderer.ts интерфейс рендера -│ ├── ThreeRenderer.ts мир на three.js (ортокамера) -│ ├── theme.ts ⭐ ВНЕШНИЙ ВИД (цвета; задел под спрайты/текстуры) +│ ├── ThreeRenderer.ts мир на three.js (псевдо-3D: наклон + биллборды) +│ ├── assets.ts ⭐ АССЕТЫ (процедурные спрайты/текстуры → текстуры three.js) +│ ├── theme.ts цвета/тинты мира (фон, эффекты) │ └── HudOverlay.ts HUD и миникарта на 2D-канвасе поверх ├── ui/ │ └── StartMenu.ts стартовое меню (DOM) с выбором уровня diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9c2f805..bc6cd0a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -105,26 +105,51 @@ - **`#hud`** — 2D-канвас поверх, `HudOverlay` рисует HP, индикатор режима, счётчик врагов, подпись комнаты, миникарту и оверлеи Game Over/Victory. -### ThreeRenderer — ортокамера и «плоское 2D» +### ThreeRenderer — псевдо-3D (наклонный вид, как в Isaac) -- `OrthographicCamera(0, CW, 0, CH, …)` отображает мировые координаты **один в - один** в пиксельные (x вправо, y вниз). Поэтому вся математика ядра валидна без - пересчётов. Слои по `z` (пол < стены < сущности < снаряды). -- Камера переворачивает ось Y → инвертируется порядок вершин → при обычном - отсечении задних граней плоскости были бы невидимы. Поэтому все материалы — - **`DoubleSide`** (правильный выбор для плоских спрайтов). +Логика остаётся 2D-сверху, но РЕНДЕР — псевдо-3D: -> **Исторический баг №3.** Именно из-за инверсии Y и отсечения граней мир рисовался -> «в пустоту» (чёрный экран при работающих draw-call). Лечится `DoubleSide`. +- **Карта координат:** игровая точка `(x, y)` кладётся на пол в 3D как `(x, 0, y)` + — пол это плоскость `Y=0`, вверх это `+Y`, «низ» игры (`y`) идёт в глубину (`Z`). + Поэтому вся математика и **коллизии ядра без изменений** — псевдо-3D чисто визуальный. +- **Камера** — `PerspectiveCamera`, наклонная и фиксированная на комнату (приподнята и + отодвинута на «юг», смотрит вниз-вперёд ≈50°). Кадрирует комнату целиком. +- **Пол** — одна горизонтальная плоскость с бесшовной текстурой (повтор по сетке). + ⚠️ Плоскость кладём ПЛАШМЯ через `flatMesh()` (поворот −90° вокруг X). Если забыть + поворот — пол встанет вертикально и перспектива «вывернется» (был такой баг). +- **Стены** — вертикальные плоскости по периметру с проёмами под двери. +- **Персонажи/враги/двери/снаряды** — ВЕРТИКАЛЬНЫЕ спрайты-биллборды (плоскости в XY, + нормаль +Z), стоящие на полу: центр на `y = высота/2`, низ на полу. Текстуры — из + `assets.ts` (см. ниже). Материалы — `DoubleSide` (наша камера переворачивает Y, иначе + грани отсеклись бы) + `alphaTest` для чёткого контура спрайта. +- **Двери всегда видимы:** рисуются из `room.doors` независимо от зачистки — закрытые + (засов) пока `!cleared`, открытый проём после. Группа комнаты пересобирается при смене + комнаты ИЛИ смене флага `cleared`. +- **Тени** — отдельный плоский спрайт-«пятно» под каждой сущностью. + +### Ассеты (render/assets.ts) + +`Assets` строит текстуры процедурно на `` (спрайты персонажей, пол, стены, +двери, снаряд, эффекты, тень) и кэширует их. Это «стандартный» набор без внешних файлов. +Заменить на свои PNG — описано в `docs/HOWTO.md` и `docs/ASSET_BRIEF.md` (бриф на полную +графику). Ключи ассетов используются и в рендере, и в брифе. + +### Эффекты оружия + +Рендер сам распознаёт события по состоянию (ядро не трогаем): +- вспышка из дула — когда `player.atkCD` «подскочил» (выстрел); +- искра — когда у врага вырос `hitTimer` (попадание); +- облачко-«пуф» — когда мёртвый враг исчезает из комнаты. +Эффекты — короткоживущие аддитивные спрайты-биллборды, гаснут по таймеру. ### Управление ресурсами GPU (важно — иначе утечки) -- Общие геометрии-«единицы» (`unitPlane`, `unitCircle`) масштабируются под размер - сущности — не плодим геометрии. -- Тайлы комнаты пересобираются **только при смене комнаты**. -- Меши сущностей создаются/удаляются по факту появления/исчезновения - (mark-and-sweep в `sync*`), их персональные материалы корректно `dispose()`-ятся. -- Общие ресурсы освобождаются один раз в `dispose()`. +- Общие геометрии переиспользуются масштабированием — не плодим геометрии. +- Группа комнаты (пол/стены/двери) пересобирается только при смене комнаты/`cleared`; + персональные материалы дверей `dispose()`-ятся при пересборке. +- Спрайты сущностей и эффекты создаются/удаляются по факту появления/исчезновения + (mark-and-sweep), их персональные материалы корректно `dispose()`-ятся. +- Общие материалы/геометрии и `Assets` освобождаются один раз в `dispose()`. ## HiDPI diff --git a/docs/ASSET_BRIEF.md b/docs/ASSET_BRIEF.md new file mode 100644 index 0000000..60d4ebe --- /dev/null +++ b/docs/ASSET_BRIEF.md @@ -0,0 +1,105 @@ +# Бриф на ассеты — «Биндим Фигняшку» + +Документ для генерации полного набора графики (через Claude/дизайн-ИИ или художника). +Ниже — **готовый промпт** (можно копировать целиком) и **таблица ассетов**, привязанная +к нашему пайплайну: каждый ассет ложится в `src/render/assets.ts` вместо текущего +процедурного рисунка (заменяем `drawX()` на загрузку PNG через `THREE.TextureLoader`). + +Следующим заходом мы подключим присланные PNG; пока в игре — простые процедурные плейсхолдеры. + +--- + +## Готовый промпт (копировать целиком) + +> Ты — художник 2D-игр. Сделай ПОЛНЫЙ набор ассетов для рогалика «Биндим Фигняшку» +> в духе The Binding of Isaac. +> +> **Игра и ракурс.** Вид сверху под наклоном (псевдо-3D): пол лежит плашмя и виден под +> углом ≈50° (камера приподнята и смотрит вниз-вперёд), а персонажи и враги — это +> ВЕРТИКАЛЬНЫЕ спрайты-биллборды, стоящие на полу и обращённые к камере (фронтальный +> вид, чуть сверху). Стены — невысокий бортик по периметру комнаты. Комната — прямоугольник +> 15×11 тайлов. +> +> **Стиль.** Мрачное подземелье, мультяшно-гротескный, читаемые силуэты, плотные тёмные +> контуры, без мелкой суеты — всё должно читаться на маленьком размере. Допустимы пиксель-арт +> ИЛИ чистый векторно-мультяшный стиль — но ЕДИНЫЙ для всего набора. +> +> **Палитра.** Пол — светлый серо-песочный камень (≈#6b6657), стены — холодный сине-серый +> кирпич (≈#47475a). Поэтому персонажи/враги должны быть НАСЫЩЕННЫМИ и контрастными, чтобы +> выделяться на светлом полу. Игрок дальнего боя — синий, ближнего — красный. Враги: «обычный» — +> тёплый терракот, «быстрый» — ярко-красный, «босс» — тёмно-багровый с рогами. +> +> **Технические требования (обязательно):** +> - PNG с прозрачным фоном (alpha), без подложки и без «приваренной» тени/пола. +> - Спрайты персонажей/врагов/дверей — ВЕРТИКАЛЬНЫЕ, соотношение 3:4 (ширина:высота), +> персонаж по центру, СТУПНИ у самого нижнего края кадра (мы «ставим» спрайт на пол по низу). +> - Текстуры пола и стен — БЕСШОВНЫЕ (tileable), повторяются по сетке. +> - Эффекты (вспышка/искра/дымок) — на прозрачном фоне, со светлым (почти белым) центром, +> чтобы их можно было тонировать цветом в движке; рассчитаны на аддитивное смешивание. +> - Никаких теней под персонажем внутри спрайта — тень рисуется отдельным ассетом. +> - Единая «толщина пикселя»/уровень детализации во всём наборе. +> +> **Что нарисовать** — см. список ниже (каждый пункт = отдельный файл с указанным именем +> и размером). Пришли файлы по этим именам (или один атлас + JSON-карта координат). + +(Дальше вставь таблицу ассетов из этого файла.) + +--- + +## Таблица ассетов + +Имена файлов соответствуют ключам в `src/render/assets.ts` — присылай ровно с такими именами. + +### Персонажи и враги (вертикальные спрайты 3:4, прозрачный фон, ступни у нижнего края) + +| Файл | Что | Рекоменд. размер | Заметки | +|---|---|---|---| +| `player-ranged.png` | Игрок, режим «дальний» (с пистолетом) | 192×256 | синий; большая голова в духе Isaac | +| `player-melee.png` | Игрок, режим «ближний» (с ножом) | 192×256 | красный; та же база, другое оружие/цвет | +| `enemy-normal.png` | Обычный враг | 192×256 | терракот, медлительный, «толстенький» | +| `enemy-fast.png` | Быстрый враг | 160×213 | ярко-красный, мельче, «дёрганый» | +| `enemy-boss.png` | Босс | 288×384 | тёмно-багровый, рога, крупный и злой | + +> Опционально (на будущее, не обязательно сейчас): по 4 ракурса на персонажа +> (вверх/вниз/влево/вправо) — тогда добавь суффиксы `-up/-down/-left/-right`. + +### Окружение + +| Файл | Что | Рекоменд. размер | Заметки | +|---|---|---|---| +| `floor.png` | Текстура пола | 256×256 | **бесшовная**, светлый серо-песочный камень, без сильных направленных деталей (виден под углом) | +| `wall.png` | Текстура стены | 256×256 | **бесшовная**, сине-серый кирпич | +| `door-closed.png` | Закрытая дверь (засов) | 192×256, 3:4 | стоит в проёме стены; видна во время боя | +| `door-open.png` | Открытая дверь (тёмный проём/арка) | 192×256, 3:4 | после зачистки комнаты | + +### Снаряды и эффекты (прозрачный фон, светлый центр, под аддитивное смешивание) + +| Файл | Что | Рекоменд. размер | Заметки | +|---|---|---|---| +| `tear.png` | Снаряд-«слеза» игрока | 64×64 | голубая светящаяся капля | +| `muzzle.png` | Вспышка из дула при выстреле | 128×128 | радиальная жёлто-белая | +| `spark.png` | Искра попадания по врагу | 128×128 | бело-жёлтая, короткая | +| `puff.png` | Облачко при гибели врага | 128×128 | серо-белое, рассеивается | +| `shadow.png` | Мягкая тень-«пятно» под сущностью | 128×64 | чёрный радиальный градиент, эллипс | + +### Интерфейс и меню (опционально, но желательно) + +| Файл | Что | Рекоменд. размер | Заметки | +|---|---|---|---| +| `logo.png` | Логотип «Биндим Фигняшку» | 900×220 | прозрачный фон, для стартового меню | +| `menu-bg.png` | Фон стартового меню | 880×660 | тёмное подземелье, не пёстрый (поверх — текст) | +| `heart-full.png` / `heart-half.png` / `heart-empty.png` | Сердечки HP | 48×48 | замена полоске HP на сердечки (как в Isaac) | +| `icon-ranged.png` / `icon-melee.png` | Иконки режимов (пистолет/нож) | 48×48 | для индикатора оружия в HUD | + +--- + +## Как мы это применим (следующий заход) + +1. Кладём PNG в `src/assets/` (новая папка) и импортируем как URL (Bun умеет в `import img from './x.png'`). +2. В `src/render/assets.ts` заменяем процедурные `drawX()` на + `new THREE.TextureLoader().load(url)` с теми же ключами — больше нигде ничего менять не нужно + (рендер берёт текстуры по ключам, см. `docs/ARCHITECTURE.md` → «Тема / ассеты»). +3. При желании заводим тему `Theme` со ссылками на наборы ассетов, чтобы переключать стиль. + +Если пришлёте ОДИН атлас вместо отдельных файлов — приложите JSON с прямоугольниками +(`{ "player-ranged": [x,y,w,h], ... }`), мы нарежем через `texture.repeat`/`offset`. diff --git a/docs/HOWTO.md b/docs/HOWTO.md index c53300b..3bee9ef 100644 --- a/docs/HOWTO.md +++ b/docs/HOWTO.md @@ -28,8 +28,10 @@ ``` 3. **`src/core/systems/spawner.ts`** — реши, когда он спавнится (логика выбора типа в начале цикла). Напр. с шансом: `rng.chance(0.15) ? 'tank' : rng.chance(ENEMY.fastChance) ? 'fast' : 'normal'`. -4. **`src/render/ThreeRenderer.ts`** — цвет в `COLOR` и выбор цвета/геометрии в - `syncEnemies` (квадрат `unitPlane` или круг `unitCircle`). +4. **`src/render/assets.ts`** — добавь ключ в `SpriteKey` и ветку в `Assets.sprite()` + (рисуется через `drawCharacter(...)` с твоими цветами). +5. **`src/render/ThreeRenderer.ts`** — добавь строку в `enemyMatKey`, сопоставив новый + `Enemy['type']` этому `SpriteKey` (без этого не пройдёт проверка типов). ИИ, урон, отбрасывание, мигание при попадании — общие, их трогать не нужно. Добавь тест в `tests/spawner.test.ts`, если ввёл особое правило спавна. @@ -111,9 +113,16 @@ --- -## 8. Кастомные ассеты (своя тема / текстуры) +## 8. Кастомные ассеты (спрайты, текстуры, тема) -Сейчас вид мира — это цвета в **`src/render/theme.ts`** (`Theme` + `DEFAULT_THEME`). +Графика (спрайты персонажей/врагов, пол, стены, двери, снаряд, эффекты, тень) рисуется +процедурно в **`src/render/assets.ts`** (класс `Assets`, кэширует текстуры three.js). + +- **Свои PNG вместо процедурных:** замени тело нужного `drawX()` на загрузку картинки + `new THREE.TextureLoader().load(url)` и верни её из соответствующего геттера — рендер + берёт текстуры по ключам, больше ничего менять не нужно. Полный список нужных ассетов с + размерами и готовый промпт для дизайн-ИИ — в **`docs/ASSET_BRIEF.md`**. +- **Цвета/тинты/фон** (не текстуры) — в **`src/render/theme.ts`** (`Theme` + `DEFAULT_THEME`). - **Своя палитра:** сделай ещё один объект `Theme` и передай его в `new ThreeRenderer(canvas, myTheme)` в `main.ts`. Сейчас рендер создаётся один @@ -133,10 +142,11 @@ Рендер изолирован за интерфейсом **`src/render/Renderer.ts`** (`render(game, alpha)` + `dispose()`). Варианты: -- **Доработать вид** (спрайты, текстуры, частицы) — внутри `ThreeRenderer`. -- **Сделать 3D** — поменяй `OrthographicCamera` на `PerspectiveCamera`, добавь - свет и 3D-меши. Мир рисуется по тем же координатам сущностей из `Game` — логику - менять не нужно. +- **Доработать вид** (спрайты, текстуры, частицы) — внутри `ThreeRenderer` (камера + уже наклонная `PerspectiveCamera`, сущности — биллборд-спрайты). +- **Усилить 3D** — для настоящего объёма замени `MeshBasicMaterial` (он без света) + на `MeshStandardMaterial`, добавь источники света и объёмные меши вместо плоскостей. + Мир рисуется по тем же координатам сущностей из `Game` — логику менять не нужно. - **Другой рендер целиком** (например, Canvas2D для отладки) — реализуй `Renderer` и подставь в `main.ts`. Ядро не трогается вообще. diff --git a/index.html b/index.html index 2c1d8b7..91cdb86 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Dungeon Crawl — three.js + Биндим Фигняшку