squad-proto/engine/tilemap.cpp
z.kirill 7d6266aaf3 Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.

ДВИЖОК (engine/, цель tile2d)

  Каталог свой, и решение «это движок» принимается тем, куда положен файл.
  Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
  и карты уехали в engine/config.h, потолок населения этажа стал своим
  (MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
  центром карты вместо позиции отряда.

  engine/projection.*  единственное место, где движок знает про изометрию;
                       видов сразу два — абстракция с одной реализацией ничем
                       не проверена и обычно оказывается неправильной
  engine/project.*     манифест проекта: пути, вид, кривая глубины
  engine/app.*         окно, кадр, fixed timestep — вместо двух копий цикла
  engine/tilemap_draw  движок научился показывать свою карту

РЕДАКТОР (editor/, цель tile2d_editor)

  Открывает любой проект и ни одной игры по имени не знает.
  --tool  одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
          этажа, ASCII-карта, проверка набора. Правку делают файлами,
          последствия читают командой (docs/19-agent.md)
  терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
          открытый проект уходит потомкам через TILE2D_PROJECT

ПРИМЕР (samples/walk, цель tile2d_walk)

  Второй потребитель движка. Игра и редактор самостоятельность движка
  проверить не могут: редактор мира не рисует, а игра — то место, куда общий
  код и уезжает незаметно.

ИГРА (src/)

  Экстракшен: вес и два порога переноски, конечный боезапас, расходники
  применяются сами, общая сумка отряда, трата опыта на улучшения.

  Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
  вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
  боец с клинком не тратил найденные патроны при запасном стволе в сумке;
  массив виджетов экрана переполнился и молча съел хвост схрона; проверка
  улучшений сама ломала забег, вызывая NewRun посреди спуска.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:07:19 +03:00

209 lines
7.8 KiB
C++
Raw Permalink 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 "engine/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;
// Захардкоженная планировка — одна тема на всю карту. Разные темы приходят
// только с уровнем, собранным из комнат (engine/level_gen.h).
for (int i = 0; i < MAP_W * MAP_H; ++i) theme[i] = 0;
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;
}