squad-proto/docs/03-rendering.md

103 lines
7.1 KiB
Markdown
Raw Permalink Normal View History

Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (raylib только окно, ввод и финальный блит). Ядро (спек, раздел 6): - LaneClear: проверка линии огня по своим, стенам и дальности; - решатель огневых позиций с гистерезисом и коммитом; - резервирование линий огня и расступание из чужих секторов; - пули-снаряды, дружественный урон физически возможен. Хоррор-слой (render-only): свет и туман войны с памятью карты, светящиеся трассеры, кровь, виньетка, зерно, напряжение. Ближний бой: сектор удара, три фазы, связка из трёх ударов, своя дисциплина «не бить сквозь своего». Проверка: squad_proto.exe --accept прогоняет критерии приёмки раздела 11 плюс блок M7 по ближнему бою — все PASS, FRIENDLY_HITS = 0 за 3 минуты боя. Сборка: cmake -S . -B build && cmake --build build --config Release raylib 6.0 подтягивается через FetchContent. Документация — CLAUDE.md как оглавление, содержание в docs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
# 03 — Рендер: софтверный буфер, изометрия, спрайты
## Фреймбуфер
Документация: границы движка, работа через модель, дизайн игры до лейтгейма 18-engine.md границы Tile2D, проекции 2D, формат проекта, второй проект 19-agent.md инструменты --tool, скиллы, терминал в редакторе 20-gdd.md акты, петли, рост отряда, закон масштаба, бюджет 200 часов 21-damage.md семь стихий, статусы, десять реакций, формулы, триггеры 22-loot.md редкости, аффиксы, артефакты, зачарования, материалы 23-endgame.md Бездна, мутации, лидерборды, 262 ачивки Плюс правки 01/02/07/15/16/17 под переехавшие пути и новые правила забега. Дизайнерская часть держится на одном законе: угроза растёт квадратично, сила отряда — линейно с затуханием, разрыв закрывается ЗНАНИЕМ, а не числами. Из него выведены и экономика опыта (41 млн против 44 млн дохода), и то, почему реакции считаются от глубины, а не от урона оружия, и почему в Бездне множитель HP разрешён, а во всём авторском контенте запрещён. .claude/commands: скиллы /tile2d, /room, /floor — правила этого движка, которые нельзя вывести из кода. Главное записано первым: не отчитываться об успехе без прохода --tool validate. Числа в этих документах — вход для инструментов, а не украшение. Расхождение между таблицей и выводом инструмента означает, что неправ документ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:45 +07:00
[engine/framebuffer.h](../engine/framebuffer.h) — `std::vector<uint32_t>` 480×270.
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (raylib только окно, ввод и финальный блит). Ядро (спек, раздел 6): - LaneClear: проверка линии огня по своим, стенам и дальности; - решатель огневых позиций с гистерезисом и коммитом; - резервирование линий огня и расступание из чужих секторов; - пули-снаряды, дружественный урон физически возможен. Хоррор-слой (render-only): свет и туман войны с памятью карты, светящиеся трассеры, кровь, виньетка, зерно, напряжение. Ближний бой: сектор удара, три фазы, связка из трёх ударов, своя дисциплина «не бить сквозь своего». Проверка: squad_proto.exe --accept прогоняет критерии приёмки раздела 11 плюс блок M7 по ближнему бою — все PASS, FRIENDLY_HITS = 0 за 3 минуты боя. Сборка: cmake -S . -B build && cmake --build build --config Release raylib 6.0 подтягивается через FetchContent. Документация — CLAUDE.md как оглавление, содержание в docs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
Формат — RGBA8, в памяти байты идут `R,G,B,A`, значит little-endian `uint32` это
`A<<24 | B<<16 | G<<8 | R`. Помощник `RGBA(r,g,b,a)` собирает цвет правильно.
Примитивы: `Clear`, `Put`, `Blit`, `BlitTinted`, `FillRect`, `Line` (Брезенхем),
`CircleOutline` (средняя точка), `CircleFilled`. Всё пишет пиксели руками.
Спрайты пишутся **альфа-тестом**: пиксель с `alpha == 0` не пишется, полупрозрачности
нет. Для эффектов есть **аддитивное** смешивание с насыщением (`AddSat`, `AddBeam`,
`AddGlowDisc`) и умножение (`ScaleColor8`, `BlitMul`), а для света — `BlitShaded`,
пропускающий цвет через световую рампу. Подробности — [10-atmosphere.md](10-atmosphere.md).
`Blit` обрезает спрайт по границам буфера ДО циклов и **выходит при пустом
диапазоне**: иначе адрес строки вычисляется за концом вектора. Цикл при этом не
выполняется, разыменования нет, но индекс уже невалиден — Debug-сборка ловит это
как `vector subscript out of range`, Release молча считает мусорный адрес.
Вывод на экран — `ScreenBlitter`: `UpdateTexture` + `DrawTexturePro` с
`TEXTURE_FILTER_POINT`. Это единственное место рендера, знающее про raylib;
`Texture2D` спрятан за `void*`, чтобы `raylib.h` не протёк в заголовок.
## Изометрия
Документация: границы движка, работа через модель, дизайн игры до лейтгейма 18-engine.md границы Tile2D, проекции 2D, формат проекта, второй проект 19-agent.md инструменты --tool, скиллы, терминал в редакторе 20-gdd.md акты, петли, рост отряда, закон масштаба, бюджет 200 часов 21-damage.md семь стихий, статусы, десять реакций, формулы, триггеры 22-loot.md редкости, аффиксы, артефакты, зачарования, материалы 23-endgame.md Бездна, мутации, лидерборды, 262 ачивки Плюс правки 01/02/07/15/16/17 под переехавшие пути и новые правила забега. Дизайнерская часть держится на одном законе: угроза растёт квадратично, сила отряда — линейно с затуханием, разрыв закрывается ЗНАНИЕМ, а не числами. Из него выведены и экономика опыта (41 млн против 44 млн дохода), и то, почему реакции считаются от глубины, а не от урона оружия, и почему в Бездне множитель HP разрешён, а во всём авторском контенте запрещён. .claude/commands: скиллы /tile2d, /room, /floor — правила этого движка, которые нельзя вывести из кода. Главное записано первым: не отчитываться об успехе без прохода --tool validate. Числа в этих документах — вход для инструментов, а не украшение. Расхождение между таблицей и выводом инструмента означает, что неправ документ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:45 +07:00
[engine/view2d.h](../engine/view2d.h). Тайл 32×16:
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (raylib только окно, ввод и финальный блит). Ядро (спек, раздел 6): - LaneClear: проверка линии огня по своим, стенам и дальности; - решатель огневых позиций с гистерезисом и коммитом; - резервирование линий огня и расступание из чужих секторов; - пули-снаряды, дружественный урон физически возможен. Хоррор-слой (render-only): свет и туман войны с памятью карты, светящиеся трассеры, кровь, виньетка, зерно, напряжение. Ближний бой: сектор удара, три фазы, связка из трёх ударов, своя дисциплина «не бить сквозь своего». Проверка: squad_proto.exe --accept прогоняет критерии приёмки раздела 11 плюс блок M7 по ближнему бою — все PASS, FRIENDLY_HITS = 0 за 3 минуты боя. Сборка: cmake -S . -B build && cmake --build build --config Release raylib 6.0 подтягивается через FetchContent. Документация — CLAUDE.md как оглавление, содержание в docs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
```
sx = (wx - wy) * 16
sy = (wx + wy) * 8
```
Обратная проекция: `wx = sx/32 + sy/16`, `wy = sy/16 - sx/32`.
`ScreenDirToWorld` переводит направление ввода из экранных осей в мировые —
именно это делает `WASD` интуитивным. Больше изометрия нигде не используется.
**Камера** следует за интерполированным якорем экспоненциальным сглаживанием
(`1 - exp(-smooth*dt)`), смещение округляется до целых пикселей — иначе
при ×3 апскейле картинка дрожит.
## Сортировка по глубине
Ключ — `wx + wy`. Пол рисуется первым сплошным проходом и в сортировке не участвует.
Все стены и все сущности попадают в **один** список `Scene::items` и сортируются
`std::stable_sort` (стабильная — чтобы порядок был детерминирован при равных ключах).
Отсечение — по экранным координатам с запасом на высоту спрайта стены.
## Точка привязки спрайта
У `Sprite` есть поля `ax, ay` — точка внутри спрайта, которая совмещается с
экранной проекцией мировой позиции. Так блит не зависит от размера спрайта:
| Спрайт | Размер | Привязка | Смысл |
|---|---|---|---|
| пол | 32×16 | (16, 8) | центр ромба = центр тайла |
| стена | 32×32 | (16, 24) | низ блока садится на ромб пола |
| агент (стрелок) | 10×14 | (5, 12) | «ноги» на земле |
| агент (клинок) | 12×15 | (6, 13) | шире в плечах, класс читается силуэтом |
| мишень | 12×16 | (6, 14) | то же |
У пули спрайта нет: снаряд рисуется светящимся трассером, см. [10-atmosphere.md](10-atmosphere.md).
## Процедурные спрайты
[src/render/sprites.cpp](../src/render/sprites.cpp), файлов с ассетами нет.
- **Пол** — ромб 32×16, два оттенка тёмно-серого металла по шахматке `(x+y)&1`,
затемнённый рант по краю и редкие светлые крапины для фактуры.
- **Стена** — верхняя грань (тот же ромб) + вертикальные грани: для каждого столбца
ищется нижняя кромка ромба и вниз заливается на высоту тайла. Левая грань темнее
правой. Обводка по силуэту — пиксели, у которых есть прозрачный 4-сосед.
- **Агент** — капсула: пиксель внутри, если расстояние до вертикального отрезка
меньше радиуса; внешний пиксель кольца — обводка, слева-сверху блик.
5 различимых цветов, по одному на агента.
- **Агент с клинком** — та же капсула, крупнее, плюс стальная вертикаль с бликом
и гардой у правого края. Цвет уже занят номером агента, поэтому класс обязан
читаться формой. Сам взмах рисуется не спрайтом, а аддитивной дугой
([11-melee.md](11-melee.md)).
- **Мишень** — та же капсула, тускло-красная, 12×16. В темноте тело не видно —
виден только тлеющий красный «глаз», нарисованный аддитивно поверх сцены.
Порядок отрисовки кадра:
```
Clear(чёрный) → свет → пол (BlitShaded) → список (стены, мишени, агенты)
→ сортировка → блит → глаза мишеней → трассеры, вспышки и дуги клинков
(аддитивно) → ОДИН пост-проход → Present → оверлей (raylib, dev-only)
```
Три инварианта, нарушение любого ломает картинку молча:
1. альфа фреймбуфера всегда 255;
2. ступень 0 световой рампы совпадает с цветом `Clear` — только поэтому полностью
тёмные тайлы можно не рисовать;
3. стена никогда не темнее самого яркого соседа-пола — иначе сквозь пропущенный
тёмный блок проступит освещённый пол тайла (tx1, ty1), который рисуется
раньше по глубине.