squad-proto/src/render/iso.h

94 lines
3.0 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
#pragma once
// -----------------------------------------------------------------------------
// Изометрия существует ТОЛЬКО здесь и в рендере. Симуляция, ИИ и коллизии
// работают в декартовом мире (top-down) и об этом файле не знают.
//
// sx = (wx - wy) * 16
// sy = (wx + wy) * 8
// -----------------------------------------------------------------------------
#include <algorithm>
#include <vector>
#include "core/math.h"
#include "render/framebuffer.h"
#include "tuning.h"
namespace iso
{
inline Vec2 WorldToScreen(Vec2 w)
{
return {(w.x - w.y) * float(TILE_HALF_W), (w.x + w.y) * float(TILE_HALF_H)};
}
inline Vec2 ScreenToWorld(Vec2 s)
{
const float a = s.x / float(TILE_W); // = (wx - wy) / 2
const float b = s.y / float(TILE_H); // = (wx + wy) / 2
return {b + a, b - a};
}
// Ключ сортировки по глубине: чем больше, тем «ближе к камере».
inline float Depth(Vec2 w) { return w.x + w.y; }
// Направление в ЭКРАННЫХ осях -> направление в мире (WASD, спек 5).
inline Vec2 ScreenDirToWorld(Vec2 screenDir)
{
return Normalized(ScreenToWorld(screenDir));
}
// Камера следует за якорем со сглаживанием; смещение округляется до пикселя.
struct Camera
{
Vec2 pos{};
bool snapped = false;
void SnapTo(Vec2 target) { pos = target; snapped = true; }
void Follow(Vec2 target, float dt, float smooth)
{
if (!snapped) { SnapTo(target); return; }
const float k = 1.0f - std::exp(-smooth * dt);
pos = Lerp(pos, target, k);
}
void ScreenOffset(int& ox, int& oy) const
{
const Vec2 c = WorldToScreen(pos);
ox = int(std::lround(INTERNAL_W * 0.5f - c.x));
oy = int(std::lround(INTERNAL_H * 0.5f - c.y));
}
};
// Один элемент списка отрисовки: и стены, и сущности идут в общий список.
struct DrawItem
{
float depth = 0.0f;
const Sprite* spr = nullptr;
int x = 0;
int y = 0;
uint32_t tint = 0;
bool hasTint = false; // явный флаг: RGBA(0,0,0,0) — легальный цвет
bool shaded = false; // рисовать через световую рампу
ShadeQuad quad{};
};
inline void SortByDepth(std::vector<DrawItem>& items)
{
std::stable_sort(items.begin(), items.end(),
[](const DrawItem& a, const DrawItem& b) { return a.depth < b.depth; });
}
inline void FlushDrawList(Framebuffer& fb, const std::vector<DrawItem>& items)
{
for (const DrawItem& it : items)
{
if (!it.spr) continue;
if (it.hasTint) fb.BlitTinted(*it.spr, it.x, it.y, it.tint);
else if (it.shaded) fb.BlitShaded(*it.spr, it.x, it.y, it.quad);
else fb.Blit(*it.spr, it.x, it.y);
}
}
} // namespace iso