feat(render): псевдо-3D, всегда видимые двери, ассеты+эффекты, русификация

- Рендер переведён в псевдо-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) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 15:16:57 +03:00
co-authored by Claude Opus 4.8
parent 5a8cf0e500
commit 3bb46e2f6b
12 changed files with 716 additions and 211 deletions
+40 -15
View File
@@ -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` строит текстуры процедурно на `<canvas>` (спрайты персонажей, пол, стены,
двери, снаряд, эффекты, тень) и кэширует их. Это «стандартный» набор без внешних файлов.
Заменить на свои 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
+105
View File
@@ -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`.
+18 -8
View File
@@ -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`. Ядро не трогается вообще.