Десять вех одним состоянием: разделять их пофайлово нечем, потому что
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>
112 lines
7.2 KiB
C++
112 lines
7.2 KiB
C++
#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). Движок сам его не ищет.
|