squad-proto/engine/projection.cpp

101 lines
3.0 KiB
C++
Raw Permalink Normal View History

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-14 01:07:19 +07:00
#include "engine/projection.h"
#include <algorithm>
#include <cstring>
namespace
{
struct ModeInfo
{
const char* name;
int tileW, tileH, wallH;
};
// Размеры подобраны так, чтобы плита занимала на экране сопоставимую площадь:
// иначе смена вида меняла бы ещё и масштаб, и сравнить два вида было бы нельзя.
const ModeInfo MODES[VIEW_MODE_COUNT] = {
{"iso", TILE_W, TILE_H, WALL_H},
{"topdown", TILE_MAX_W, TILE_MAX_H, TILE_MAX_H},
};
} // namespace
const char* ViewModeName(ViewMode m)
{
const int i = int(m);
return (i >= 0 && i < VIEW_MODE_COUNT) ? MODES[i].name : "?";
}
bool ParseViewMode(const char* name, ViewMode& out)
{
if (!name) return false;
for (int i = 0; i < VIEW_MODE_COUNT; ++i)
if (std::strcmp(name, MODES[i].name) == 0)
{
out = ViewMode(i);
return true;
}
return false;
}
Projection Projection::For(ViewMode m)
{
const ModeInfo& info = MODES[int(m) < VIEW_MODE_COUNT ? int(m) : 0];
Projection p;
p.mode = m;
p.tileW = info.tileW;
p.tileH = info.tileH;
p.wallH = info.wallH;
return p;
}
Vec2 Projection::WorldToScreen(Vec2 w) const
{
if (mode == ViewMode::TOP_DOWN) return {w.x * float(tileW), w.y * float(tileH)};
// sx = (wx - wy) * tileW/2
// sy = (wx + wy) * tileH/2
return {(w.x - w.y) * float(tileW) * 0.5f, (w.x + w.y) * float(tileH) * 0.5f};
}
Vec2 Projection::ScreenToWorld(Vec2 s) const
{
if (mode == ViewMode::TOP_DOWN) return {s.x / float(tileW), s.y / float(tileH)};
const float a = s.x / float(tileW); // = (wx - wy) / 2
const float b = s.y / float(tileH); // = (wx + wy) / 2
return {b + a, b - a};
}
float Projection::Depth(Vec2 w) const
{
// В изометрии «дальше от камеры» — это меньшая сумма координат; при виде
// сверху глубины как таковой нет, и порядок задаёт только строка экрана.
return (mode == ViewMode::TOP_DOWN) ? w.y : (w.x + w.y);
}
void Projection::TileRow(int y, int& x0, int& x1) const
{
if (mode == ViewMode::TOP_DOWN)
{
x0 = 0;
x1 = tileW;
return;
}
// Ступенька ромба — tileW/(tileH/2), то есть ровно наклон 2:1.
const int half = std::max(1, tileH / 2);
const int k = (y < half) ? y : (tileH - 1 - y);
const int width = std::max(1, (tileW / half) * (k + 1));
x0 = (tileW - width) / 2;
x1 = x0 + width;
}
int Projection::WallAnchorY() const
{
// Изометрия: низ блока садится на ромб пола, поэтому крыша поднята над
// центром плиты на высоту тайла. Вид сверху: подниматься некуда.
return (mode == ViewMode::TOP_DOWN) ? tileH / 2 : tileH / 2 + tileH;
}