squad-proto/engine/projection.h

76 lines
4.4 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
#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;