Десять вех одним состоянием: разделять их пофайлово нечем, потому что
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>
76 lines
4.4 KiB
C++
76 lines
4.4 KiB
C++
#pragma once
|
||
// -----------------------------------------------------------------------------
|
||
// Проекция: как мир кладётся на экран.
|
||
//
|
||
// Это ЕДИНСТВЕННОЕ место, где движок знает про изометрию. Мир везде декартов
|
||
// (тайл = 1.0 юнита), симуляция, ИИ и коллизии об этом файле не знают вообще —
|
||
// правило «src/ai/ не знает про изометрию» этим и держится.
|
||
//
|
||
// Проекция задаёт четыре вещи, и больше ничего:
|
||
// * куда мировая точка попадает на экран и обратно;
|
||
// * чем сортируется список отрисовки (кто «ближе к камере»);
|
||
// * какой формы плита пола — ромб или квадрат;
|
||
// * насколько крыша стены поднята над своей плитой.
|
||
//
|
||
// Всё остальное — спрайты, свет, кровь, эффекты — считается ЧЕРЕЗ эти четыре
|
||
// ответа и потому переживает смену вида. Абстракция с одной реализацией была бы
|
||
// самообманом, поэтому режима сразу два: изометрия 2:1 и вид сверху.
|
||
//
|
||
// Вид выбирается ОДИН раз при старте (проектом или ключом --view) и дальше не
|
||
// меняется: тайлсет рисуется под форму плиты, и смена вида на ходу означала бы
|
||
// перерисовку всех спрайтов посреди кадра.
|
||
// -----------------------------------------------------------------------------
|
||
#include <cstdint>
|
||
|
||
#include "engine/config.h"
|
||
#include "engine/math.h"
|
||
|
||
enum class ViewMode : uint8_t
|
||
{
|
||
ISO_2_1 = 0, // ромб 32x16, наклон 2:1 — классическая изометрия
|
||
TOP_DOWN, // квадрат 32x32, вид строго сверху
|
||
};
|
||
|
||
constexpr int VIEW_MODE_COUNT = 2;
|
||
|
||
const char* ViewModeName(ViewMode m);
|
||
// Разбор имени из project.txt или из ключа --view. false — имя не опознано,
|
||
// out не тронут (звать с уже осмысленным значением по умолчанию).
|
||
bool ParseViewMode(const char* name, ViewMode& out);
|
||
|
||
struct Projection
|
||
{
|
||
ViewMode mode = ViewMode::ISO_2_1;
|
||
|
||
int tileW = TILE_W; // размер плиты пола на экране
|
||
int tileH = TILE_H;
|
||
int wallH = WALL_H; // высота спрайта стены целиком
|
||
|
||
static Projection For(ViewMode m);
|
||
|
||
// Мир -> экран и обратно. Обратная строго обратна прямой: по ней считается
|
||
// направление WASD, и рассогласование увело бы отряд не туда, куда жмут.
|
||
Vec2 WorldToScreen(Vec2 w) const;
|
||
Vec2 ScreenToWorld(Vec2 s) const;
|
||
|
||
// Ключ сортировки: чем больше, тем ближе к камере.
|
||
float Depth(Vec2 w) const;
|
||
|
||
// Занятая часть строки y у плиты пола: ромб или вся строка.
|
||
void TileRow(int y, int& x0, int& x1) const;
|
||
|
||
// Куда внутри спрайта стены попадает центр её плиты. В изометрии крыша
|
||
// поднята над полом на высоту тайла, при виде сверху подниматься некуда.
|
||
int WallAnchorY() const;
|
||
|
||
// Есть ли у стены видимые вертикальные грани. При виде сверху их нет, и
|
||
// рисовать их значило бы показывать стену в двух проекциях сразу.
|
||
bool WallHasSides() const { return mode == ViewMode::ISO_2_1; }
|
||
};
|
||
|
||
// Активная проекция. Глобальная по той же причине, что и g_tune: её читают
|
||
// рендер, редактор и генерация тайлов, и протаскивать её параметром через
|
||
// каждый блит значило бы переписать половину рендера ради значения, которое
|
||
// за весь запуск не меняется ни разу.
|
||
inline Projection g_proj;
|