squad-proto/engine/framebuffer.h
z.kirill 7d6266aaf3 Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.

ДВИЖОК (engine/, цель tile2d)

  Каталог свой, и решение «это движок» принимается тем, куда положен файл.
  Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
  и карты уехали в engine/config.h, потолок населения этажа стал своим
  (MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
  центром карты вместо позиции отряда.

  engine/projection.*  единственное место, где движок знает про изометрию;
                       видов сразу два — абстракция с одной реализацией ничем
                       не проверена и обычно оказывается неправильной
  engine/project.*     манифест проекта: пути, вид, кривая глубины
  engine/app.*         окно, кадр, fixed timestep — вместо двух копий цикла
  engine/tilemap_draw  движок научился показывать свою карту

РЕДАКТОР (editor/, цель tile2d_editor)

  Открывает любой проект и ни одной игры по имени не знает.
  --tool  одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
          этажа, ASCII-карта, проверка набора. Правку делают файлами,
          последствия читают командой (docs/19-agent.md)
  терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
          открытый проект уходит потомкам через TILE2D_PROJECT

ПРИМЕР (samples/walk, цель tile2d_walk)

  Второй потребитель движка. Игра и редактор самостоятельность движка
  проверить не могут: редактор мира не рисует, а игра — то место, куда общий
  код и уезжает незаметно.

ИГРА (src/)

  Экстракшен: вес и два порога переноски, конечный боезапас, расходники
  применяются сами, общая сумка отряда, трата опыта на улучшения.

  Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
  вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
  боец с клинком не тратил найденные патроны при запасном стволе в сумке;
  массив виджетов экрана переполнился и молча съел хвост схрона; проверка
  улучшений сама ломала забег, вызывая NewRun посреди спуска.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:07:19 +03:00

204 lines
8.8 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
// -----------------------------------------------------------------------------
// Софтверный фреймбуфер: RGBA8, ручная запись пикселей.
// Ни один игровой пиксель не рисуется примитивами raylib.
// raylib используется только в ScreenBlitter — финальный блит на экран.
//
// Инварианты (нарушение любого ломает картинку молча):
// * альфа фреймбуфера всегда 255;
// * ступень 0 световой рампы даёт чистый чёрный и совпадает с цветом Clear;
// * спрайты пишутся с альфа-тестом, эффекты — аддитивно.
// -----------------------------------------------------------------------------
#include <algorithm>
#include <cstdint>
#include <vector>
// В памяти байты идут R,G,B,A (PIXELFORMAT_UNCOMPRESSED_R8G8B8A8),
// значит little-endian uint32 = A<<24 | B<<16 | G<<8 | R.
constexpr uint32_t RGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255)
{
return uint32_t(r) | (uint32_t(g) << 8) | (uint32_t(b) << 16) | (uint32_t(a) << 24);
}
constexpr uint8_t AlphaOf(uint32_t c) { return uint8_t(c >> 24); }
// Цвет для аддитивных эффектов: альфа 0, чтобы такой цвет нельзя было случайно
// использовать как непрозрачный пиксель спрайта.
constexpr uint32_t FXC(uint8_t r, uint8_t g, uint8_t b) { return RGBA(r, g, b, 0); }
// Сложение цветов с насыщением по каждому каналу.
inline uint32_t AddSat(uint32_t a, uint32_t b)
{
uint32_t r = (a & 0xFFu) + (b & 0xFFu);
uint32_t g = ((a >> 8) & 0xFFu) + ((b >> 8) & 0xFFu);
uint32_t bl = ((a >> 16) & 0xFFu) + ((b >> 16) & 0xFFu);
if (r > 255u) r = 255u;
if (g > 255u) g = 255u;
if (bl > 255u) bl = 255u;
return 0xFF000000u | r | (g << 8) | (bl << 16);
}
// Умножение цвета на коэффициент в формате 8.8 (256 == x1.0).
inline uint32_t ScaleColor8(uint32_t c, uint32_t k256)
{
uint32_t r = ((c & 0xFFu) * k256) >> 8;
uint32_t g = (((c >> 8) & 0xFFu) * k256) >> 8;
uint32_t bl = (((c >> 16) & 0xFFu) * k256) >> 8;
if (r > 255u) r = 255u;
if (g > 255u) g = 255u;
if (bl > 255u) bl = 255u;
return 0xFF000000u | r | (g << 8) | (bl << 16);
}
// -----------------------------------------------------------------------------
// Световая рампа (по мотивам COLORMAP из Doom).
//
// Свет — одна скалярная величина на точку мира. Таблица переводит её в цвет:
// в полумраке картинка уходит в холодный синий, на свету нейтральна, во вспышке
// выбивается в тёплый пересвет. На пиксель остаются три выборки из таблицы
// и ноль умножений.
// -----------------------------------------------------------------------------
constexpr int RAMP_LEVELS = 32;
extern uint8_t g_shadeLut[RAMP_LEVELS][3][256];
// Перепечь таблицу. Вызывать при изменении гаммы/усиления, иначе ручки молчат.
void BuildShadeRamp(float gamma, float maxGain);
// Упорядоченный дизеринг: 32 ступени рампы выглядят как ~200, полос не видно.
extern const uint8_t BAYER4[4][4];
inline uint32_t ShadePixel(uint32_t c, int lvl)
{
const uint8_t* lr = g_shadeLut[lvl][0];
const uint8_t* lg = g_shadeLut[lvl][1];
const uint8_t* lb = g_shadeLut[lvl][2];
return 0xFF000000u | uint32_t(lr[c & 0xFFu]) | (uint32_t(lg[(c >> 8) & 0xFFu]) << 8) |
(uint32_t(lb[(c >> 16) & 0xFFu]) << 16);
}
// Билинейный патч света по спрайту: L(px,py) = a + b*px + c*py + e*px^2 + f*py^2.
// Такая форма — это ТОЧНАЯ билинейка по четырём углам ромба тайла, записанная
// в координатах спрайта; поэтому на общем ребре соседних тайлов значения
// совпадают и швов нет.
struct ShadeQuad
{
float a = 1.0f;
float b = 0.0f;
float c = 0.0f;
float e = 0.0f;
float f = 0.0f;
// Строки ниже clampRow светятся как clampRow: вертикальные грани стены
// берут свет нижней кромки своей крыши, а не экстраполяцию квадратичного
// члена на 32 строки вниз.
int clampRow = 1 << 24;
static ShadeQuad Flat(float level)
{
ShadeQuad q;
q.a = level;
return q;
}
};
// Спрайт с альфа-тестом: px[i] с alpha==0 не пишется. Блендинга нет.
// (ax, ay) — точка привязки внутри спрайта: она совмещается с экранной
// проекцией мировой позиции сущности.
struct Sprite
{
int w = 0;
int h = 0;
int ax = 0;
int ay = 0;
std::vector<uint32_t> px;
void Init(int width, int height, uint32_t fill = 0)
{
w = width;
h = height;
px.assign(size_t(w) * size_t(h), fill);
}
void Set(int x, int y, uint32_t c)
{
if (unsigned(x) < unsigned(w) && unsigned(y) < unsigned(h)) px[size_t(y) * w + x] = c;
}
uint32_t Get(int x, int y) const
{
if (unsigned(x) < unsigned(w) && unsigned(y) < unsigned(h)) return px[size_t(y) * w + x];
return 0;
}
};
struct Framebuffer
{
int w = 0;
int h = 0;
std::vector<uint32_t> px;
void Init(int width, int height);
void Clear(uint32_t color);
inline void Put(int x, int y, uint32_t c)
{
if (unsigned(x) < unsigned(w) && unsigned(y) < unsigned(h)) px[size_t(y) * w + x] = c;
}
// Блит с альфа-тестом; (x, y) — куда попадёт точка привязки спрайта.
void Blit(const Sprite& s, int x, int y);
// Тот же блит, но каждый непрозрачный пиксель заменяется на tint (для силуэтов).
void BlitTinted(const Sprite& s, int x, int y, uint32_t tint);
// Блит со светом: цвет каждого пикселя проходит через рампу.
void BlitShaded(const Sprite& s, int x, int y, const ShadeQuad& q);
// Умножение фона под маской спрайта (контактные тени, лужи).
void BlitMul(const Sprite& mask, int x, int y, uint32_t k256);
void FillRect(int x, int y, int rw, int rh, uint32_t c);
void Line(int x0, int y0, int x1, int y1, uint32_t c);
void CircleOutline(int cx, int cy, int r, uint32_t c);
void CircleFilled(int cx, int cy, int r, uint32_t c);
// --- аддитивные примитивы для эффектов ---
inline void AddPx(int x, int y, uint32_t c)
{
if (unsigned(x) < unsigned(w) && unsigned(y) < unsigned(h))
{
uint32_t& d = px[size_t(y) * w + x];
d = AddSat(d, c);
}
}
// Светящийся штрих: яркое ядро + боковой ореол, цвет плывёт от head к tail.
void AddBeam(float x0, float y0, float x1, float y1, uint32_t headCol, uint32_t tailCol,
int halo);
// Мягкое пятно света (вспышка у дула, искра попадания).
void AddGlowDisc(int cx, int cy, int radius, uint32_t color);
};
// -----------------------------------------------------------------------------
// Финальный вывод: UpdateTexture + DrawTexturePro, фильтрация POINT.
// Единственное место рендера, знающее про raylib.
// -----------------------------------------------------------------------------
// Куда лёг кадр внутри окна. Оверлей и UI обязаны считать координаты ЧЕРЕЗ него:
// на borderless fullscreen кадр центрирован и вокруг него чёрные поля.
struct ScreenRect
{
int x = 0, y = 0, w = 0, h = 0;
int scale = 1;
};
ScreenRect ScreenFitRect(int fbW, int fbH, int windowW, int windowH);
struct ScreenBlitter
{
void Init(int width, int height);
void Shutdown();
void Present(const Framebuffer& fb, int windowW, int windowH) const;
private:
void* tex = nullptr; // Texture2D на куче — чтобы не тащить raylib.h в заголовок
int texW = 0;
int texH = 0;
};