squad-proto/engine/framebuffer.cpp

355 lines
12 KiB
C++
Raw Normal View History

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-14 01:07:19 +07:00
#include "engine/framebuffer.h"
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
#include <raylib.h>
#include <cmath>
#include <cstdlib>
// -----------------------------------------------------------------------------
// Световая рампа
// -----------------------------------------------------------------------------
uint8_t g_shadeLut[RAMP_LEVELS][3][256];
const uint8_t BAYER4[4][4] = {
{ 0, 8, 2, 10},
{12, 4, 14, 6},
{ 3, 11, 1, 9},
{15, 7, 13, 5},
};
namespace
{
// Цвет полумрака и цвет пересвета. Между ними рампа плавно интерполирует.
constexpr float COLD[3] = {0.60f, 0.74f, 1.00f}; // холодный синий в тенях
constexpr float WARM[3] = {1.00f, 0.92f, 0.76f}; // тёплый пересвет во вспышке
inline float Smooth01(float t) { return t * t * (3.0f - 2.0f * t); }
struct ShadeRampInit
{
ShadeRampInit() { BuildShadeRamp(1.35f, 1.10f); }
};
ShadeRampInit g_rampInit;
} // namespace
void BuildShadeRamp(float gamma, float maxGain)
{
for (int lvl = 0; lvl < RAMP_LEVELS; ++lvl)
{
// Ступень 0 — чистый чёрный: она обязана совпадать с цветом Clear,
// иначе невидимые тайлы нельзя пропускать при отрисовке.
if (lvl == 0)
{
for (int ch = 0; ch < 3; ++ch)
for (int v = 0; v < 256; ++v) g_shadeLut[lvl][ch][v] = 0;
continue;
}
const float t = float(lvl) / float(RAMP_LEVELS - 1);
const float gain = std::pow(t, gamma) * maxGain;
const float mix = Smooth01(t);
for (int ch = 0; ch < 3; ++ch)
{
const float tint = COLD[ch] + (WARM[ch] - COLD[ch]) * mix;
const float k = gain * tint;
for (int v = 0; v < 256; ++v)
{
const float out = float(v) * k;
g_shadeLut[lvl][ch][v] = uint8_t(out < 0.0f ? 0.0f : (out > 255.0f ? 255.0f : out));
}
}
}
}
// -----------------------------------------------------------------------------
void Framebuffer::Init(int width, int height)
{
w = width;
h = height;
px.assign(size_t(w) * size_t(h), 0u);
}
void Framebuffer::Clear(uint32_t color)
{
std::fill(px.begin(), px.end(), color);
}
namespace
{
// Общая обрезка спрайта по буферу. false — рисовать нечего.
inline bool ClipSprite(const Sprite& s, int x, int y, int fbW, int fbH, int& dstX0, int& dstY0,
int& sx0, int& sy0, int& sx1, int& sy1)
{
dstX0 = x - s.ax;
dstY0 = y - s.ay;
sx0 = std::max(0, -dstX0);
sy0 = std::max(0, -dstY0);
sx1 = std::min(s.w, fbW - dstX0);
sy1 = std::min(s.h, fbH - dstY0);
// Пустой диапазон -> выходим ДО вычисления адресов: иначе индекс уезжает
// за границы vector (Debug ловит это как "vector subscript out of range").
return sx0 < sx1 && sy0 < sy1;
}
} // namespace
void Framebuffer::Blit(const Sprite& s, int x, int y)
{
int dstX0, dstY0, sx0, sy0, sx1, sy1;
if (!ClipSprite(s, x, y, w, h, dstX0, dstY0, sx0, sy0, sx1, sy1)) return;
for (int sy = sy0; sy < sy1; ++sy)
{
const uint32_t* src = s.px.data() + size_t(sy) * s.w + sx0;
uint32_t* dst = px.data() + size_t(dstY0 + sy) * w + (dstX0 + sx0);
for (int sx = sx0; sx < sx1; ++sx, ++src, ++dst)
{
const uint32_t c = *src;
if (AlphaOf(c) != 0) *dst = c; // альфа-тест, без блендинга
}
}
}
void Framebuffer::BlitTinted(const Sprite& s, int x, int y, uint32_t tint)
{
int dstX0, dstY0, sx0, sy0, sx1, sy1;
if (!ClipSprite(s, x, y, w, h, dstX0, dstY0, sx0, sy0, sx1, sy1)) return;
for (int sy = sy0; sy < sy1; ++sy)
for (int sx = sx0; sx < sx1; ++sx)
if (AlphaOf(s.px[size_t(sy) * s.w + sx]) != 0)
px[size_t(dstY0 + sy) * w + (dstX0 + sx)] = tint;
}
void Framebuffer::BlitShaded(const Sprite& s, int x, int y, const ShadeQuad& q)
{
int dstX0, dstY0, sx0, sy0, sx1, sy1;
if (!ClipSprite(s, x, y, w, h, dstX0, dstY0, sx0, sy0, sx1, sy1)) return;
constexpr float MAX_LVL = float(RAMP_LEVELS - 1);
for (int sy = sy0; sy < sy1; ++sy)
{
const float py = float(sy < q.clampRow ? sy : q.clampRow);
const float rowBase = q.a + q.c * py + q.f * py * py;
// Прямые разности по строке: во внутреннем цикле два сложения.
const float fx0 = float(sx0);
float lit = rowBase + q.b * fx0 + q.e * fx0 * fx0;
float dl = q.b + q.e * (2.0f * fx0 + 1.0f);
const float ddl = 2.0f * q.e;
const uint8_t* bayerRow = BAYER4[(dstY0 + sy) & 3];
const uint32_t* src = s.px.data() + size_t(sy) * s.w + sx0;
uint32_t* dst = px.data() + size_t(dstY0 + sy) * w + (dstX0 + sx0);
for (int sx = sx0; sx < sx1; ++sx, ++src, ++dst)
{
const uint32_t c = *src;
if (AlphaOf(c) != 0)
{
const float dither = (float(bayerRow[(dstX0 + sx) & 3]) + 0.5f) * (1.0f / 16.0f);
int lvl = int(lit * MAX_LVL + dither);
if (lvl < 0) lvl = 0;
else if (lvl >= RAMP_LEVELS) lvl = RAMP_LEVELS - 1;
if (lvl != 0) *dst = ShadePixel(c, lvl);
}
lit += dl;
dl += ddl;
}
}
}
void Framebuffer::BlitMul(const Sprite& mask, int x, int y, uint32_t k256)
{
int dstX0, dstY0, sx0, sy0, sx1, sy1;
if (!ClipSprite(mask, x, y, w, h, dstX0, dstY0, sx0, sy0, sx1, sy1)) return;
for (int sy = sy0; sy < sy1; ++sy)
for (int sx = sx0; sx < sx1; ++sx)
if (AlphaOf(mask.px[size_t(sy) * mask.w + sx]) != 0)
{
uint32_t& d = px[size_t(dstY0 + sy) * w + (dstX0 + sx)];
d = ScaleColor8(d, k256);
}
}
void Framebuffer::FillRect(int x, int y, int rw, int rh, uint32_t c)
{
const int x0 = std::max(0, x);
const int y0 = std::max(0, y);
const int x1 = std::min(w, x + rw);
const int y1 = std::min(h, y + rh);
for (int yy = y0; yy < y1; ++yy)
for (int xx = x0; xx < x1; ++xx)
px[size_t(yy) * w + xx] = c;
}
void Framebuffer::Line(int x0, int y0, int x1, int y1, uint32_t c)
{
// Брезенхем. Put сам отсекает по границам.
int dx = std::abs(x1 - x0);
int dy = -std::abs(y1 - y0);
const int sx = (x0 < x1) ? 1 : -1;
const int sy = (y0 < y1) ? 1 : -1;
int err = dx + dy;
for (;;)
{
Put(x0, y0, c);
if (x0 == x1 && y0 == y1) break;
const int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
void Framebuffer::CircleOutline(int cx, int cy, int r, uint32_t c)
{
int x = r;
int y = 0;
int err = 1 - r;
while (x >= y)
{
Put(cx + x, cy + y, c); Put(cx + y, cy + x, c);
Put(cx - y, cy + x, c); Put(cx - x, cy + y, c);
Put(cx - x, cy - y, c); Put(cx - y, cy - x, c);
Put(cx + y, cy - x, c); Put(cx + x, cy - y, c);
++y;
if (err < 0) err += 2 * y + 1;
else { --x; err += 2 * (y - x) + 1; }
}
}
void Framebuffer::CircleFilled(int cx, int cy, int r, uint32_t c)
{
const int r2 = r * r;
for (int y = -r; y <= r; ++y)
for (int x = -r; x <= r; ++x)
if (x * x + y * y <= r2) Put(cx + x, cy + y, c);
}
// -----------------------------------------------------------------------------
// Аддитивные примитивы
// -----------------------------------------------------------------------------
void Framebuffer::AddBeam(float x0, float y0, float x1, float y1, uint32_t headCol,
uint32_t tailCol, int halo)
{
// Отсечение по расширенному прямоугольнику кадра: длинный закадровый штрих
// не должен крутить тысячи холостых итераций.
const float pad = float(halo) + 2.0f;
if ((x0 < -pad && x1 < -pad) || (x0 > float(w) + pad && x1 > float(w) + pad)) return;
if ((y0 < -pad && y1 < -pad) || (y0 > float(h) + pad && y1 > float(h) + pad)) return;
const float dx = x1 - x0;
const float dy = y1 - y0;
const float len = std::sqrt(dx * dx + dy * dy);
const int steps = int(len) + 1;
if (steps > 4 * (w + h)) return; // защита от вырожденных значений
const float inv = 1.0f / float(steps);
for (int i = 0; i <= steps; ++i)
{
const float t = float(i) * inv; // 0 = хвост, 1 = голова
const float px_ = x0 + dx * t;
const float py_ = y0 + dy * t;
// Цвет плывёт от хвоста к голове; хвост дополнительно гаснет.
const uint32_t core = AddSat(ScaleColor8(tailCol, uint32_t((1.0f - t) * 256.0f)),
ScaleColor8(headCol, uint32_t(t * 256.0f)));
const int ix = int(px_);
const int iy = int(py_);
AddPx(ix, iy, core);
// Боковой ореол: та же линия вполсилы на соседних пикселях.
if (halo > 0)
{
const uint32_t glow = ScaleColor8(core, 96);
AddPx(ix + 1, iy, glow);
AddPx(ix - 1, iy, glow);
AddPx(ix, iy + 1, glow);
AddPx(ix, iy - 1, glow);
}
}
}
void Framebuffer::AddGlowDisc(int cx, int cy, int radius, uint32_t color)
{
if (radius <= 0) return;
if (cx + radius < 0 || cx - radius >= w || cy + radius < 0 || cy - radius >= h) return;
const float inv = 1.0f / float(radius);
for (int y = -radius; y <= radius; ++y)
{
for (int x = -radius; x <= radius; ++x)
{
const float d = std::sqrt(float(x * x + y * y)) * inv;
if (d >= 1.0f) continue;
const float k = (1.0f - d) * (1.0f - d); // квадратичное затухание
AddPx(cx + x, cy + y, ScaleColor8(color, uint32_t(k * 256.0f)));
}
}
}
// -----------------------------------------------------------------------------
void ScreenBlitter::Init(int width, int height)
{
Image img = GenImageColor(width, height, BLANK);
ImageFormat(&img, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8);
Texture2D* t = new Texture2D(LoadTextureFromImage(img));
UnloadImage(img);
SetTextureFilter(*t, TEXTURE_FILTER_POINT);
tex = t;
texW = width;
texH = height;
}
void ScreenBlitter::Shutdown()
{
if (!tex) return;
Texture2D* t = static_cast<Texture2D*>(tex);
UnloadTexture(*t);
delete t;
tex = nullptr;
}
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-14 01:07:19 +07:00
// Прямоугольник кадра в окне: ЦЕЛЫЙ масштаб и центрирование.
//
// На borderless fullscreen окно почти никогда не кратно 480x270, и дробное
// растяжение размывает пиксель: одни строки удваиваются, соседние нет.
// Поэтому берём наибольший целый масштаб, а остаток отдаём чёрным полям.
ScreenRect ScreenFitRect(int fbW, int fbH, int windowW, int windowH)
{
if (fbW <= 0 || fbH <= 0) return {};
int scale = std::min(windowW / fbW, windowH / fbH);
if (scale < 1) scale = 1; // окно меньше кадра: лучше обрезать, чем мылить
ScreenRect r;
r.scale = scale;
r.w = fbW * scale;
r.h = fbH * scale;
r.x = (windowW - r.w) / 2;
r.y = (windowH - r.h) / 2;
return r;
}
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
void ScreenBlitter::Present(const Framebuffer& fb, int windowW, int windowH) const
{
if (!tex) return;
if (fb.w != texW || fb.h != texH) return; // размер разошёлся — не грузим мусор
Texture2D* t = static_cast<Texture2D*>(tex);
UpdateTexture(*t, fb.px.data());
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-14 01:07:19 +07:00
const ScreenRect fit = ScreenFitRect(fb.w, fb.h, windowW, windowH);
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
const Rectangle src = {0.0f, 0.0f, float(fb.w), float(fb.h)};
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-14 01:07:19 +07:00
const Rectangle dst = {float(fit.x), float(fit.y), float(fit.w), float(fit.h)};
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
DrawTexturePro(*t, src, dst, {0.0f, 0.0f}, 0.0f, WHITE);
}