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);
|
|
|
|
|
|
}
|