squad-proto/engine/catalog.h

112 lines
7.2 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
// -----------------------------------------------------------------------------
// Каталог размещаемых сущностей: ЧТО вообще можно поставить в комнату и на
// какой глубине это встречается.
//
// Это граница между движком и игрой. Редактор про EnemyKind и ItemId не знает
// и знать не может — он линкуется только с движком. Всё, что он видит, —
// строковые id из этого каталога: `shambler`, `crate`, `plate`. Игра переводит
// id в свои перечисления одной таблицей (sim/spawn_catalog.h), и это
// ЕДИНСТВЕННОЕ место перевода.
//
// Почему не общий enum на движок и игру: тогда добавление твари правило бы
// заголовок движка, редактор пришлось бы пересобирать вместе с игрой, а
// «движок» превратился бы в подкаталог игры. Здесь же новая тварь — это строка
// в текстовом файле плюс одна строка в таблице перевода.
//
// --- почему формат key=value -------------------------------------------------
// Первая версия каталога была позиционной: цвет, радиус и спрайт стояли по
// местам. Игра про бесконечный спуск потребовала добавить сущности глубину,
// опасность и качество, и позиционный формат на этом кончился: каждое новое
// свойство ломало бы все уже написанные строки. Ключи же дописываются с краю
// и не трогают ничего.
// -----------------------------------------------------------------------------
#include <cstdint>
#include <string>
#include <vector>
// Роль сущности. Редактору важны эти три случая: они по-разному ставятся и
// по-разному попадают в уровень.
enum class EntityClass : uint8_t
{
MONSTER = 0, // тварь: ходит, бьёт, участвует в зачистке и тратит бюджет опасности
PROP, // обстановка: ящик, бочка, шкафчик. Опасности не несёт
LOOT, // предмет: лежит на полу, подбирается на ходу
};
const char* EntityClassName(EntityClass cls);
struct EntityDesc
{
std::string id; // то, что попадает в файл комнаты
std::string name; // подпись в палитре редактора
EntityClass cls = EntityClass::MONSTER;
uint8_t r = 180, g = 180, b = 180;
float radius = 0.34f;
// Имя ассета SpriteForge (`sf list`), пусто — рисовать капсулу по цвету.
// Имя, а не числовой id: каталог читают данными, а generated-заголовок
// с константами SF_ASSET_* принадлежит игре, а не движку.
std::string sprite;
// --- глубина --------------------------------------------------------------
// С какого этажа сущность вообще может появиться. Это главный рычаг всей
// игры: «чем глубже, тем страшнее» — это в первую очередь ЗДЕСЬ, а не в
// множителях урона. Громила на первом этаже ломает не баланс, а обещание.
int minDepth = 1;
// По какой этаж включительно; 0 — до бесконечности. Нужен, чтобы мелочь
// первых этажей перестала занимать место на глубине.
int maxDepth = 0;
// Цена в бюджете опасности этажа (engine/progression.h). Не урон и не HP,
// а «сколько места в голове игрока эта тварь занимает»: бюджет — это то,
// чем этаж отличается по СЛОЖНОСТИ, а не по числу тел.
float danger = 1.0f;
// Качество предмета для class=loot. Этаж выдаёт лут своего тира и ниже,
// поэтому карабин не исчезает на глубине — он просто перестаёт быть
// находкой.
int tier = 0;
bool InDepth(int depth) const
{
return depth >= minDepth && (maxDepth <= 0 || depth <= maxDepth);
}
};
// Порядок записей — порядок палитры в редакторе. Он же порядок в файле:
// каталог правится руками, и перетасовывать его при загрузке нельзя.
struct EntityCatalog
{
std::vector<EntityDesc> entries;
// Разобрать текстовый каталог. false — файла нет или он битый; err (если
// передан) получает первую же осечку с номером строки.
bool Load(const std::string& path, std::string* err = nullptr);
int Count() const { return int(entries.size()); }
const EntityDesc& At(int i) const { return entries[size_t(i)]; }
// -1 — такого id в каталоге нет. Комната, ссылающаяся на выпавший id,
// не должна ронять ни редактор, ни игру: точка просто не сработает.
int IndexOf(const std::string& id) const;
const EntityDesc* Find(const std::string& id) const;
// Сколько записей данного класса. Палитра редактора разбита на вкладки.
int CountOf(EntityClass cls) const;
// Есть ли на этой глубине хоть одна тварь и хоть один предмет. Отвечает на
// единственный вопрос, который может убить забег: «а есть ли чем населить
// сотый этаж».
bool HasAnyAt(EntityClass cls, int depth) const;
// Самая глубокая точка, дальше которой каталог перестаёт что-либо
// добавлять: максимум minDepth по всем записям. Дальше неё этаж собирается
// из того же набора, и это нормально — бесконечность не требует
// бесконечного каталога, она требует, чтобы он не кончался.
int DeepestIntroduction() const;
};
// Где лежит каталог — знает проект (engine/project.h). Движок сам его не ищет.