squad-proto/src/render/sprites.cpp

203 lines
6.5 KiB
C++
Raw 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
#include "render/sprites.h"
#include "core/math.h"
namespace
{
// Занятая часть строки y для ромба 32x16.
void DiamondRow(int y, int& x0, int& x1)
{
const int k = (y < TILE_HALF_H) ? y : (TILE_H - 1 - y);
const int width = 4 * (k + 1);
x0 = TILE_HALF_W - width / 2;
x1 = x0 + width;
}
// Обводка по силуэту: любой непрозрачный пиксель, у которого есть прозрачный
// 4-сосед (или край спрайта), перекрашивается в outline.
void OutlineSilhouette(Sprite& s, uint32_t outline)
{
Sprite copy = s;
for (int y = 0; y < s.h; ++y)
{
for (int x = 0; x < s.w; ++x)
{
if (AlphaOf(copy.Get(x, y)) == 0) continue;
const bool border =
x == 0 || y == 0 || x == s.w - 1 || y == s.h - 1 ||
AlphaOf(copy.Get(x - 1, y)) == 0 || AlphaOf(copy.Get(x + 1, y)) == 0 ||
AlphaOf(copy.Get(x, y - 1)) == 0 || AlphaOf(copy.Get(x, y + 1)) == 0;
if (border) s.Set(x, y, outline);
}
}
}
Sprite MakeFloor(uint32_t base)
{
Sprite s;
s.Init(TILE_W, TILE_H, 0);
s.ax = TILE_HALF_W;
s.ay = TILE_HALF_H;
const uint32_t edge = ShadeColor(base, 0.78f);
const uint32_t fleck = ShadeColor(base, 1.18f);
for (int y = 0; y < TILE_H; ++y)
{
int x0 = 0, x1 = 0;
DiamondRow(y, x0, x1);
for (int x = x0; x < x1; ++x)
{
uint32_t c = base;
if (x < x0 + 2 || x >= x1 - 2) c = edge; // рант тайла
else if (((x * 7 + y * 13) % 23) == 0) c = fleck; // «металлическая» рябь
s.Set(x, y, c);
}
}
return s;
}
Sprite MakeWall(uint32_t top)
{
Sprite s;
s.Init(TILE_W, WALL_H, 0);
s.ax = TILE_HALF_W;
s.ay = TILE_HALF_H + TILE_H; // низ блока садится на ромб пола
const uint32_t left = ShadeColor(top, 0.62f);
const uint32_t right = ShadeColor(top, 0.80f);
// верхняя грань
for (int y = 0; y < TILE_H; ++y)
{
int x0 = 0, x1 = 0;
DiamondRow(y, x0, x1);
for (int x = x0; x < x1; ++x) s.Set(x, y, top);
}
// вертикальные грани: от нижней кромки ромба вниз на высоту тайла
for (int x = 0; x < TILE_W; ++x)
{
int bottom = -1;
for (int y = 0; y < TILE_H; ++y)
if (AlphaOf(s.Get(x, y)) != 0) bottom = y;
if (bottom < 0) continue;
const uint32_t side = (x < TILE_HALF_W) ? left : right;
for (int y = bottom + 1; y <= bottom + TILE_H && y < WALL_H; ++y)
s.Set(x, y, side);
}
OutlineSilhouette(s, ShadeColor(top, 0.35f));
return s;
}
// Капсула с обводкой и бликом.
Sprite MakeCapsule(int w, int h, float radius, uint32_t body)
{
Sprite s;
s.Init(w, h, 0);
s.ax = w / 2;
s.ay = h - 2; // «ноги» чуть выше нижнего края
const uint32_t outline = ShadeColor(body, 0.35f);
const uint32_t hilite = ShadeColor(body, 1.35f);
const uint32_t shadow = ShadeColor(body, 0.72f);
const float cx = (w - 1) * 0.5f;
const Vec2 a{cx, radius - 0.5f};
const Vec2 b{cx, h - radius - 0.5f};
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
const float d = DistancePointToSegment({float(x), float(y)}, a, b);
if (d > radius) continue;
if (d > radius - 1.0f) { s.Set(x, y, outline); continue; }
uint32_t c = body;
if (float(x) < cx - 0.5f && y < h / 2) c = hilite;
else if (float(x) > cx + 1.0f) c = shadow;
s.Set(x, y, c);
}
}
return s;
}
// Боец ближнего боя: та же капсула, но шире в плечах и с клинком у бедра.
// На 480x270 класс обязан читаться силуэтом — цвет уже занят номером агента.
Sprite MakeMeleeCapsule(int w, int h, float radius, uint32_t body)
{
Sprite s = MakeCapsule(w, h, radius, body);
const uint32_t steel = RGBA(196, 206, 222);
const uint32_t edge = RGBA(240, 246, 255);
// Клинок: короткая вертикаль вдоль правого края силуэта, с бликом.
const int bx = w - 2;
for (int y = 2; y < h - 4; ++y)
{
s.Set(bx, y, steel);
if (y < h / 2) s.Set(bx, y, edge);
}
// Гарда, чтобы клинок не читался как случайная полоска.
s.Set(bx - 1, h - 5, steel);
s.Set(bx, h - 5, steel);
return s;
}
// Стрелок с пистолетом: та же капсула, но уже в плечах и с коротким стволом
// у пояса. Класс оружия обязан читаться силуэтом — цвет уже занят номером.
Sprite MakePistolCapsule(int w, int h, float radius, uint32_t body)
{
Sprite s = MakeCapsule(w, h, radius, body);
const uint32_t steel = RGBA(150, 156, 168);
const uint32_t dark = RGBA(70, 74, 84);
// Коротышка-ствол: две пиксельных полоски у бедра, с рукоятью вниз.
const int bx = w - 2;
const int by = h - 7;
s.Set(bx, by, steel);
s.Set(bx - 1, by, steel);
s.Set(bx, by + 1, dark);
s.Set(bx, by + 2, dark);
return s;
}
} // namespace
uint32_t ShadeColor(uint32_t c, float k)
{
const float r = Clampf(float(c & 0xFF) * k, 0.0f, 255.0f);
const float g = Clampf(float((c >> 8) & 0xFF) * k, 0.0f, 255.0f);
const float b = Clampf(float((c >> 16) & 0xFF) * k, 0.0f, 255.0f);
return RGBA(uint8_t(r), uint8_t(g), uint8_t(b), AlphaOf(c));
}
void SpriteSet::Generate()
{
floorA = MakeFloor(RGBA(50, 54, 62));
floorB = MakeFloor(RGBA(42, 46, 53));
wall = MakeWall(RGBA(78, 84, 96));
// 5 различимых цветов — по одному на агента
static const uint32_t agentCols[NUM_AGENTS] = {
RGBA(80, 200, 220),
RGBA(232, 176, 72),
RGBA(116, 204, 116),
RGBA(212, 116, 200),
RGBA(116, 148, 240),
};
for (int i = 0; i < NUM_AGENTS; ++i)
{
agent[i] = MakeCapsule(10, 14, 4.0f, agentCols[i]);
melee[i] = MakeMeleeCapsule(12, 15, 4.6f, agentCols[i]);
pistol[i] = MakePistolCapsule(9, 13, 3.6f, agentCols[i]);
}
target = MakeCapsule(12, 16, 5.0f, RGBA(120, 44, 44));
}