Десять вех одним состоянием: разделять их пофайлово нечем, потому что
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>
101 lines
3.0 KiB
C++
101 lines
3.0 KiB
C++
#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;
|
||
}
|