#pragma once // ----------------------------------------------------------------------------- // Каталог размещаемых сущностей: ЧТО вообще можно поставить в комнату и на // какой глубине это встречается. // // Это граница между движком и игрой. Редактор про EnemyKind и ItemId не знает // и знать не может — он линкуется только с движком. Всё, что он видит, — // строковые id из этого каталога: `shambler`, `crate`, `plate`. Игра переводит // id в свои перечисления одной таблицей (sim/spawn_catalog.h), и это // ЕДИНСТВЕННОЕ место перевода. // // Почему не общий enum на движок и игру: тогда добавление твари правило бы // заголовок движка, редактор пришлось бы пересобирать вместе с игрой, а // «движок» превратился бы в подкаталог игры. Здесь же новая тварь — это строка // в текстовом файле плюс одна строка в таблице перевода. // // --- почему формат key=value ------------------------------------------------- // Первая версия каталога была позиционной: цвет, радиус и спрайт стояли по // местам. Игра про бесконечный спуск потребовала добавить сущности глубину, // опасность и качество, и позиционный формат на этом кончился: каждое новое // свойство ломало бы все уже написанные строки. Ключи же дописываются с краю // и не трогают ничего. // ----------------------------------------------------------------------------- #include #include #include // Роль сущности. Редактору важны эти три случая: они по-разному ставятся и // по-разному попадают в уровень. 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 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). Движок сам его не ищет.