squad-proto/src/world/tilemap.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

206 lines
7.5 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 "world/tilemap.h"
#include <limits>
namespace
{
constexpr float INF_F = std::numeric_limits<float>::infinity();
} // namespace
// -----------------------------------------------------------------------------
// Уровень. Всё захардкожено, ни одного случайного числа — карта детерминирована.
//
// y 0..12 — северная комната (кучка мишеней)
// y 13..15 — стена-разделитель с двумя проходами шириной 2 тайла
// y 16..45 — центральная арена + западная и восточная комнаты за стенами
// -----------------------------------------------------------------------------
void Tilemap::Generate()
{
for (int i = 0; i < MAP_W * MAP_H; ++i) tiles[i] = Tile::EMPTY;
auto fill = [&](int x0, int y0, int x1, int y1, Tile t) {
for (int y = y0; y <= y1; ++y)
for (int x = x0; x <= x1; ++x)
Set(x, y, t);
};
auto column = [&](int x, int y) { Set(x, y, Tile::WALL); };
// --- сплошная стена по периметру (2 тайла) ---
fill(0, 0, MAP_W - 1, 1, Tile::WALL);
fill(0, MAP_H - 2, MAP_W - 1, MAP_H - 1, Tile::WALL);
fill(0, 0, 1, MAP_H - 1, Tile::WALL);
fill(MAP_W - 2, 0, MAP_W - 1, MAP_H - 1, Tile::WALL);
// --- северная стена-разделитель, два прохода шириной 2 ---
fill(2, 13, 45, 15, Tile::WALL);
fill(10, 13, 11, 15, Tile::EMPTY);
fill(30, 13, 31, 15, Tile::EMPTY);
// --- западная стена-разделитель, проход шириной 2 ---
fill(12, 20, 14, 44, Tile::WALL);
fill(12, 30, 14, 31, Tile::EMPTY);
// --- восточная стена-разделитель, проход шириной 2 ---
fill(38, 18, 40, 40, Tile::WALL);
fill(38, 27, 40, 28, Tile::EMPTY);
// --- колонны и огрызки стен на арене (без них не проверить LOS) ---
const int arenaCols[][2] = {
{18, 19}, {18, 27}, {18, 34}, {24, 19}, {24, 34},
{30, 28}, {30, 34}, {26, 24}, {21, 23}, {35, 22}, {35, 36},
};
for (const auto& c : arenaCols) column(c[0], c[1]);
fill(20, 30, 22, 30, Tile::WALL); // огрызок-стенка поперёк
fill(27, 38, 27, 40, Tile::WALL); // огрызок вдоль
fill(33, 30, 34, 31, Tile::WALL); // блок 2x2
// --- северная комната ---
const int northCols[][2] = {{8, 8}, {14, 5}, {30, 9}, {38, 6}, {42, 10}, {17, 10}, {35, 4}};
for (const auto& c : northCols) column(c[0], c[1]);
fill(21, 9, 23, 9, Tile::WALL);
// --- западная комната ---
const int westCols[][2] = {{5, 25}, {8, 30}, {4, 35}, {10, 38}, {6, 21}};
for (const auto& c : westCols) column(c[0], c[1]);
fill(4, 28, 4, 30, Tile::WALL);
// --- восточная комната ---
const int eastCols[][2] = {{43, 22}, {42, 30}, {44, 38}};
for (const auto& c : eastCols) column(c[0], c[1]);
// --- южная полоса ---
const int southCols[][2] = {{20, 43}, {28, 42}, {34, 44}, {16, 41}};
for (const auto& c : southCols) column(c[0], c[1]);
}
// -----------------------------------------------------------------------------
// DDA Amanatides & Woo.
// -----------------------------------------------------------------------------
bool Tilemap::RaycastBlocked(Vec2 a, Vec2 b, float* tHit) const
{
int x = int(std::floor(a.x));
int y = int(std::floor(a.y));
if (IsWall(x, y)) { if (tHit) *tHit = 0.0f; return true; }
const Vec2 d = b - a;
const float len = Length(d);
if (len < 1e-6f) return false;
const Vec2 dir = d / len;
const int stepX = (dir.x > 0.0f) ? 1 : ((dir.x < 0.0f) ? -1 : 0);
const int stepY = (dir.y > 0.0f) ? 1 : ((dir.y < 0.0f) ? -1 : 0);
const float tDeltaX = (stepX != 0) ? std::fabs(1.0f / dir.x) : INF_F;
const float tDeltaY = (stepY != 0) ? std::fabs(1.0f / dir.y) : INF_F;
float tMaxX = INF_F;
float tMaxY = INF_F;
if (stepX > 0) tMaxX = (float(x + 1) - a.x) / dir.x;
else if (stepX < 0) tMaxX = (float(x) - a.x) / dir.x;
if (stepY > 0) tMaxY = (float(y + 1) - a.y) / dir.y;
else if (stepY < 0) tMaxY = (float(y) - a.y) / dir.y;
const int endX = int(std::floor(b.x));
const int endY = int(std::floor(b.y));
for (;;)
{
if (x == endX && y == endY) return false;
float tEnter;
if (tMaxX < tMaxY)
{
if (tMaxX > len) return false;
tEnter = tMaxX;
x += stepX;
tMaxX += tDeltaX;
}
else
{
if (tMaxY > len) return false;
tEnter = tMaxY;
y += stepY;
tMaxY += tDeltaY;
}
if (IsWall(x, y))
{
if (tHit) *tHit = Clampf(tEnter / len, 0.0f, 1.0f);
return true;
}
}
}
// -----------------------------------------------------------------------------
// Circle vs AABB тайлов: выталкивание по кратчайшему направлению.
// -----------------------------------------------------------------------------
void Tilemap::ResolveCircle(Vec2& p, float r) const
{
for (int iter = 0; iter < 3; ++iter)
{
bool moved = false;
const int x0 = int(std::floor(p.x - r));
const int x1 = int(std::floor(p.x + r));
const int y0 = int(std::floor(p.y - r));
const int y1 = int(std::floor(p.y + r));
for (int ty = y0; ty <= y1; ++ty)
{
for (int tx = x0; tx <= x1; ++tx)
{
if (!IsWall(tx, ty)) continue;
const Vec2 closest{Clampf(p.x, float(tx), float(tx + 1)),
Clampf(p.y, float(ty), float(ty + 1))};
Vec2 delta = p - closest;
const float dist = Length(delta);
if (dist > 1e-5f)
{
if (dist >= r) continue;
p += (delta / dist) * (r - dist);
moved = true;
}
else
{
// центр внутри тайла — выталкиваем по ближайшей грани
const float dl = p.x - float(tx);
const float dr = float(tx + 1) - p.x;
const float dt = p.y - float(ty);
const float db = float(ty + 1) - p.y;
const float m = std::min(std::min(dl, dr), std::min(dt, db));
if (m == dl) p.x = float(tx) - r;
else if (m == dr) p.x = float(tx + 1) + r;
else if (m == dt) p.y = float(ty) - r;
else p.y = float(ty + 1) + r;
moved = true;
}
}
}
if (!moved) break;
}
}
bool Tilemap::CircleHitsWall(Vec2 p, float r) const
{
const int x0 = int(std::floor(p.x - r));
const int x1 = int(std::floor(p.x + r));
const int y0 = int(std::floor(p.y - r));
const int y1 = int(std::floor(p.y + r));
for (int ty = y0; ty <= y1; ++ty)
{
for (int tx = x0; tx <= x1; ++tx)
{
if (!IsWall(tx, ty)) continue;
const Vec2 closest{Clampf(p.x, float(tx), float(tx + 1)),
Clampf(p.y, float(ty), float(ty + 1))};
if (DistanceSq(p, closest) < r * r) return true;
}
}
return false;
}