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

112 lines
7.2 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
// -----------------------------------------------------------------------------
// Каталог размещаемых сущностей: ЧТО вообще можно поставить в комнату и на
// какой глубине это встречается.
//
// Это граница между движком и игрой. Редактор про 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). Движок сам его не ищет.