squad-proto/editor/editor.h

176 lines
8.7 KiB
C
Raw 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
// -----------------------------------------------------------------------------
// Редактор комнат — ОТДЕЛЬНОЕ приложение поверх движка.
//
// Он линкуется только с tile2d и не видит ни одного символа из sim/ и ai/.
// Поэтому «редактор знает про правила боя» — это ошибка линковки, а не то, что
// надо ловить на ревью.
//
// Ни одной игры по имени он тоже не знает: где содержимое, говорит проект
// (engine/project.h), а что можно ставить в комнату — его каталог сущностей.
//
// Рисует он ровно тем же софтверным фреймбуфером и той же ПРОЕКЦИЕЙ, что и
// игра: комната в редакторе выглядит так же, как в бою, потому что это один и
// тот же код, а не похожий. В проекте с видом сверху редактор тоже показывает
// вид сверху — иначе он планировал бы не то, что получится.
//
// Раскладка кадра 480x270:
// y 0..11 верхняя панель: файл, операции над комнатами
// x 0..379 холст: изометрия комнаты (или карта предпросмотра уровня)
// x 380..479 правая панель: кисти, свойства комнаты, палитра
// y 232..269 нижняя полоса поверх холста: таблица выбранной точки спавна
// -----------------------------------------------------------------------------
#include <string>
#include <vector>
#include "engine/math.h"
#include "engine/catalog.h"
#include "engine/level_gen.h"
#include "engine/project.h"
#include "engine/room.h"
#include "engine/tileset.h"
#include "engine/framebuffer.h"
#include "engine/sfa_library.h"
// Кисть. Пол, стена и дверь пишут в сетку; спавн ставит точку; ластик снимает
// и то и другое — иначе «убрать» требовало бы помнить, чем это поставлено.
enum class Tool : uint8_t
{
FLOOR = 0,
WALL,
DOOR,
SPAWN,
ERASE,
COUNT,
};
// Что делает элемент интерфейса. Действие, а не «номер кнопки»: раскладка
// панелей меняется чаще, чем набор операций.
enum class Act : uint8_t
{
NONE = 0,
TOOL, PALETTE,
ROOM_PREV, ROOM_NEXT, ROOM_NEW, ROOM_SAVE, ROOM_RELOAD,
W_DEC, W_INC, H_DEC, H_INC,
WEIGHT_DEC, WEIGHT_INC, THEME_CYCLE, START_TOGGLE,
DMIN_DEC, DMIN_INC, DMAX_DEC, DMAX_INC,
PALETTE_TAB,
CHANCE_DEC, CHANCE_INC, OPT_DEC, OPT_INC,
PREVIEW_TOGGLE, PREVIEW_RESEED, PREVIEW_DEEPER, PREVIEW_SHALLOWER, PLAYTEST,
};
struct EdRect
{
int x = 0, y = 0, w = 0, h = 0;
bool Has(float fx, float fy) const
{
return fx >= float(x) && fy >= float(y) && fx < float(x + w) && fy < float(y + h);
}
};
struct EdWidget
{
Act act = Act::NONE;
int arg = 0; // номер кисти, индекс каталога, ...
EdRect rect;
std::string label;
bool active = false; // включённое состояние (нажатая кисть)
bool enabled = true;
};
struct Editor
{
// --- данные ---------------------------------------------------------------
// Проект — первое, что поднимается: из него берутся пути к содержимому,
// вид отображения и кривая глубины. Редактор не знает ни одной игры по
// имени и открывает тот проект, который ему назвали.
Project project;
EntityCatalog catalog;
RoomLibrary lib;
TileSet tiles;
SpriteLibrary forge;
bool forgeOpen = false;
// Рабочая копия правится, а lib.rooms остаётся тем, что лежит на диске:
// без этого «не сохранять» было бы нечем осуществить.
Room room;
int roomIndex = -1;
bool dirty = false;
// --- инструменты ----------------------------------------------------------
Tool tool = Tool::WALL;
int paletteIndex = 0;
// Каталог вырос до трёх десятков записей и в столбик больше не помещается.
// Вкладки по классу — не украшение: твари, обстановка и лут ставятся в
// разные моменты работы, и мешать их в одном списке значит листать чужое.
EntityClass paletteTab = EntityClass::MONSTER;
int paletteScroll = 0;
int selX = -1, selY = -1; // выбранная точка спавна; -1 — не выбрана
// --- вид ------------------------------------------------------------------
int panX = 0, panY = 0; // сдвиг холста в пикселях
int hoverX = -1, hoverY = -1;
bool painting = false; // ЛКМ зажата: кисть тянется
bool erasing = false;
// --- предпросмотр этажа ---------------------------------------------------
bool preview = false;
uint32_t seed = 1;
// Какой этаж собирается в предпросмотре. Отдельно от диапазона комнаты:
// проверять надо не только «попадает ли комната», но и «во что превращается
// весь набор» на этой глубине.
int previewDepth = 1;
Tilemap previewMap;
GeneratedLevel previewLevel;
std::string previewError;
// --- строка состояния -----------------------------------------------------
std::string status;
float statusTimer = 0.0f;
std::vector<EdWidget> widgets;
// Поднять проект, каталог, набор комнат и тайлы. false — работать не с чем
// (нет проекта или каталога сущностей), причина уходит в status.
// projectName — имя или путь; пусто — единственный найденный проект.
bool Init(const std::string& projectName = {});
// Ввод и действия. fit — куда лёг фреймбуфер в окне.
void Update(const ScreenRect& fit, float dt);
// Комната (или карта предпросмотра) в софтверный буфер.
void DrawWorld(Framebuffer& fb);
// Панели поверх готового кадра: примитивы raylib, потому что здесь нужны
// слова, и много.
void DrawUi(const ScreenRect& fit) const;
// Выполнить действие интерфейса. Публично, потому что через него же
// работают ключи командной строки: «показать сборку уровня» с клавиши и
// из аргумента обязаны делать буквально одно и то же.
void Apply(Act act, int arg);
private:
// u — целый масштаб кадра в окне. Нужен, чтобы мерить подписи ТЕМ ЖЕ
// шрифтом и кеглем, каким они будут нарисованы: у встроенного шрифта raylib
// межбуквенный интервал зависит от размера, и посчитанная «по пять пикселей
// на букву» ширина кнопки разъезжается с настоящей.
void BuildWidgets(int u);
void LoadRoomAt(int index);
void SaveCurrent();
void ReloadLibrary();
void NewRoom();
void PaintAt(int x, int y, bool erase);
void Regenerate();
void Say(const std::string& s);
// Спрайт-маркер сущности каталога: кадр из SpriteForge, если он собран,
// иначе капсула по цвету и радиусу. Пустая палитра не должна мешать
// планировать уровень.
const Sprite* MarkerSprite(int catalogIndex) const;
mutable std::vector<Sprite> markers; // кеш, по индексу каталога
};