squad-proto/src/render/scene.cpp

102 lines
3.3 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/scene.h"
namespace
{
// Запас за краями кадра, чтобы не отсекать спрайты, торчащие внутрь.
constexpr int CULL_MARGIN_X = TILE_W;
constexpr int CULL_MARGIN_Y = WALL_H + TILE_H;
inline bool OffScreen(int sx, int sy)
{
return sx < -CULL_MARGIN_X || sx > INTERNAL_W + CULL_MARGIN_X ||
sy < -CULL_MARGIN_Y || sy > INTERNAL_H + CULL_MARGIN_Y;
}
} // namespace
void Scene::Begin(const iso::Camera& cam)
{
items.clear();
cam.ScreenOffset(ox, oy);
}
void Scene::Add(const Sprite* spr, Vec2 world, int pixelLift, float depthBias)
{
const Vec2 s = iso::WorldToScreen(world);
const int sx = int(std::lround(s.x)) + ox;
const int sy = int(std::lround(s.y)) + oy - pixelLift;
if (OffScreen(sx, sy)) return;
iso::DrawItem it;
it.depth = iso::Depth(world) + depthBias;
it.spr = spr;
it.x = sx;
it.y = sy;
items.push_back(it);
}
void Scene::AddShaded(const Sprite* spr, Vec2 world, const ShadeQuad& quad, int pixelLift,
float depthBias)
{
const Vec2 s = iso::WorldToScreen(world);
const int sx = int(std::lround(s.x)) + ox;
const int sy = int(std::lround(s.y)) + oy - pixelLift;
if (OffScreen(sx, sy)) return;
iso::DrawItem it;
it.depth = iso::Depth(world) + depthBias;
it.spr = spr;
it.x = sx;
it.y = sy;
it.shaded = true;
it.quad = quad;
items.push_back(it);
}
void Scene::Flush(Framebuffer& fb)
{
iso::SortByDepth(items);
iso::FlushDrawList(fb, items);
}
// Ниже этого уровня рампа даёт ступень 0, то есть чистый чёрный.
static const float DARK_CUTOFF = 0.5f / float(RAMP_LEVELS - 1);
void DrawFloor(Framebuffer& fb, const SpriteSet& sprites, const Tilemap& map, int ox, int oy,
const Lighting& light, const BloodField* blood)
{
for (int ty = 0; ty < MAP_H; ++ty)
{
for (int tx = 0; tx < MAP_W; ++tx)
{
if (map.IsWall(tx, ty)) continue; // под стеной пол не виден
if (light.MaxCornerOf(tx, ty) < DARK_CUTOFF) continue;
const Vec2 c = iso::WorldToScreen({tx + 0.5f, ty + 0.5f});
const int sx = int(std::lround(c.x)) + ox;
const int sy = int(std::lround(c.y)) + oy;
if (OffScreen(sx, sy)) continue;
const Sprite* stained = blood ? blood->At(tx, ty) : nullptr;
const Sprite& s = stained ? *stained : sprites.FloorTile(tx, ty);
fb.BlitShaded(s, sx, sy, light.QuadFor(tx, ty, TILE_H - 1));
}
}
}
void CollectWalls(Scene& scene, const SpriteSet& sprites, const Tilemap& map,
const Lighting& light)
{
for (int ty = 0; ty < MAP_H; ++ty)
for (int tx = 0; tx < MAP_W; ++tx)
{
if (!map.IsWall(tx, ty)) continue;
if (light.MaxCornerOf(tx, ty) < DARK_CUTOFF) continue;
// clampRow = TILE_H-1: вертикальные грани светятся как нижняя кромка
// крыши, а не как экстраполяция патча на 32 строки вниз.
scene.AddShaded(&sprites.wall, {tx + 0.5f, ty + 0.5f},
light.QuadFor(tx, ty, TILE_H - 1));
}
}