squad-proto/src/render/atmos.cpp
z.kirill 7880f275bd Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (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 13:00:49 +03:00

84 lines
4.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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