squad-proto/engine/projection.h
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

76 lines
4.4 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.

#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;