squad-proto/src/render/atmos.cpp

84 lines
4.2 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/atmos.h"
#include "render/iso.h"
#include "render/noise.h"
namespace
{
// Мировой квадрат вокруг камеры, гарантированно покрывающий кадр 480x270.
// В изометрии |wx-wy| <= 15 по ширине и |wx+wy| <= 17 по высоте, так что 19
// тайлов в каждую сторону — с запасом на подъём пылинки по экрану.
constexpr int ASH_REACH = 19;
// Больше трёх пылинок на тайл — это уже не воздух, а снегопад.
constexpr int ASH_PER_CELL = 3;
constexpr uint32_t COL_ASH = FXC(198, 186, 166); // тёплый серый: пепел, не снег
} // namespace
void Atmosphere::DrawAsh(Framebuffer& fb, Vec2 camPos, int ox, int oy, const Lighting& light) const
{
const float density = g_tune.ashDensity;
const float bright = g_tune.ashBrightness;
if (density <= 0.0f || bright <= 0.0f) return;
const int cx = int(std::floor(camPos.x));
const int cy = int(std::floor(camPos.y));
const float drift = g_tune.ashSpeed;
for (int gy = cy - ASH_REACH; gy <= cy + ASH_REACH; ++gy)
{
for (int gx = cx - ASH_REACH; gx <= cx + ASH_REACH; ++gx)
{
for (int k = 0; k < ASH_PER_CELL; ++k)
{
// Дробная плотность: последней пылинке в ячейке «положено»
// столько, сколько осталось от density.
const float quota = density - float(k);
if (quota <= 0.0f) break;
const uint32_t h = Hash3(uint32_t(gx + 4096), uint32_t(gy + 4096),
uint32_t(k) * 977u + 13u);
if (quota < 1.0f && Hash01(h) > quota) continue;
const Vec2 base{float(gx) + Hash01(Hash2(h, 1u)),
float(gy) + Hash01(Hash2(h, 2u))};
// Дрейф — замкнутая петля, а не бесконечный снос: иначе пылинку
// пришлось бы «заворачивать», и она телепортировалась бы на глазах.
const float ph = Hash01(Hash2(h, 3u)) * TWO_PI_F;
const float rate = 0.20f + 0.45f * Hash01(Hash2(h, 4u));
const float amp = 0.30f + 0.30f * Hash01(Hash2(h, 5u));
const float t = time * drift * rate;
const Vec2 p{base.x + std::cos(t * TWO_PI_F + ph) * amp,
base.y + std::sin(t * TWO_PI_F * 0.73f + ph * 1.7f) * amp};
const float lit = light.LightAt(p);
if (lit < 0.05f) continue;
// Высота: часть взвеси стелется по полу, часть висит на уровне
// головы. Медленное покачивание вверх-вниз добавляет объёма.
const float liftFrac = 0.15f + 0.95f * Hash01(Hash2(h, 6u));
const float bob = std::sin(t * TWO_PI_F * 1.31f + ph) * 2.5f;
const int lift = int(g_tune.ashLiftPx * liftFrac + bob);
const Vec2 s = iso::WorldToScreen(p);
const int sx = int(std::lround(s.x)) + ox;
const int sy = int(std::lround(s.y)) + oy - lift;
if (unsigned(sx) >= unsigned(fb.w) || unsigned(sy) >= unsigned(fb.h)) continue;
// Мерцание: пылинка поворачивается к свету то одним боком, то другим.
const float twinkle = 0.45f + 0.55f * (0.5f + 0.5f * std::sin(t * 9.0f + ph * 3.0f));
const float amount = lit * bright * twinkle * (0.35f + 0.65f * Hash01(Hash2(h, 8u)));
const uint32_t col = ScaleColor8(COL_ASH, uint32_t(Clampf(amount, 0.0f, 1.0f) * 256.0f));
// Крупные хлопья редки и получают ореол — глазу есть за что зацепиться.
if ((h & 15u) == 0u) fb.AddGlowDisc(sx, sy, 1, col);
else fb.AddPx(sx, sy, col);
}
}
}
}