squad-proto/engine/catalog.cpp

216 lines
6.9 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
#include "engine/catalog.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include "engine/text_parse.h"
namespace fs = std::filesystem;
namespace
{
// color=116,52,52 — три байта через запятую. Отдельным ключом, а не тремя:
// цвет это одно свойство, и разносить его по трём ключам значило бы разрешить
// написать два из трёх.
bool ParseColor(const std::string& v, EntityDesc& out)
{
const size_t c1 = v.find(',');
if (c1 == std::string::npos) return false;
const size_t c2 = v.find(',', c1 + 1);
if (c2 == std::string::npos) return false;
out.r = uint8_t(text::ToInt(v.substr(0, c1), 180));
out.g = uint8_t(text::ToInt(v.substr(c1 + 1, c2 - c1 - 1), 180));
out.b = uint8_t(text::ToInt(v.substr(c2 + 1), 180));
return true;
}
// depth=3 с третьего этажа и глубже
// depth=1..4 с первого по четвёртый включительно
bool ParseDepth(const std::string& v, EntityDesc& out)
{
const size_t dots = v.find("..");
if (dots == std::string::npos)
{
out.minDepth = std::max(1, text::ToInt(v, 1));
out.maxDepth = 0;
return true;
}
out.minDepth = std::max(1, text::ToInt(v.substr(0, dots), 1));
out.maxDepth = text::ToInt(v.substr(dots + 2), 0);
return out.maxDepth <= 0 || out.maxDepth >= out.minDepth;
}
// entity <id> ключ=значение ...
bool ParseEntity(const std::vector<std::string>& p, EntityDesc& out, std::string& why)
{
if (p.size() < 2)
{
why = "у записи нет id";
return false;
}
out.id = p[1];
out.name = p[1];
bool haveClass = false;
for (size_t i = 2; i < p.size(); ++i)
{
std::string key, value;
if (!text::SplitKeyValue(p[i], key, value) || key.empty() || value.empty())
{
why = "не ключ=значение: " + p[i];
return false;
}
if (key == "class")
{
if (value == "monster") out.cls = EntityClass::MONSTER;
else if (value == "prop") out.cls = EntityClass::PROP;
else if (value == "loot") out.cls = EntityClass::LOOT;
else { why = "неизвестный класс: " + value; return false; }
haveClass = true;
}
else if (key == "name") out.name = value;
else if (key == "sprite") out.sprite = (value == "-") ? std::string{} : value;
else if (key == "radius") out.radius = text::ToFloat(value, 0.34f);
else if (key == "danger") out.danger = text::ToFloat(value, 1.0f);
else if (key == "tier") out.tier = text::ToInt(value, 0);
else if (key == "color")
{
if (!ParseColor(value, out)) { why = "цвет не разбирается: " + value; return false; }
}
else if (key == "depth")
{
if (!ParseDepth(value, out)) { why = "глубина не разбирается: " + value; return false; }
}
else
{
// Молча глотать незнакомый ключ нельзя: опечатка в `dangre=3`
// означала бы тварь с ценой по умолчанию и ровно ноль признаков
// того, что что-то не так.
why = "неизвестный ключ: " + key;
return false;
}
}
if (!haveClass)
{
why = "не указан class";
return false;
}
// Обстановка и предметы в бюджет опасности не входят по определению:
// ящик не делает этаж страшнее, он делает его обжитее.
if (out.cls != EntityClass::MONSTER) out.danger = 0.0f;
return true;
}
} // namespace
const char* EntityClassName(EntityClass cls)
{
switch (cls)
{
case EntityClass::MONSTER: return "MONSTER";
case EntityClass::PROP: return "PROP";
default: return "LOOT";
}
}
bool EntityCatalog::Load(const std::string& path, std::string* err)
{
std::ifstream in(path);
if (!in)
{
if (err) *err = "cannot open catalog: " + path;
return false;
}
// Разбираем в СВОЙ вектор и подменяем только на успехе: битая правка
// каталога не должна оставлять редактор с половиной палитры.
std::vector<EntityDesc> parsed;
std::string line;
int lineNo = 0;
while (std::getline(in, line))
{
++lineNo;
const std::vector<std::string> p = text::Split(text::StripComment(line));
if (p.empty()) continue;
if (p[0] != "entity")
{
if (err) *err = "line " + std::to_string(lineNo) + ": expected `entity`";
return false;
}
EntityDesc d;
std::string why;
if (!ParseEntity(p, d, why))
{
if (err) *err = "line " + std::to_string(lineNo) + ": " + why;
return false;
}
// Дубль id молча перекрыл бы предыдущую запись, и в редакторе появились
// бы две одинаковые кнопки палитры с разным поведением.
for (const EntityDesc& prev : parsed)
{
if (prev.id != d.id) continue;
if (err) *err = "line " + std::to_string(lineNo) + ": duplicate id `" + d.id + "`";
return false;
}
parsed.push_back(std::move(d));
}
if (parsed.empty())
{
if (err) *err = "empty catalog: " + path;
return false;
}
entries = std::move(parsed);
return true;
}
int EntityCatalog::IndexOf(const std::string& id) const
{
for (size_t i = 0; i < entries.size(); ++i)
if (entries[i].id == id) return int(i);
return -1;
}
const EntityDesc* EntityCatalog::Find(const std::string& id) const
{
const int i = IndexOf(id);
return (i >= 0) ? &entries[size_t(i)] : nullptr;
}
int EntityCatalog::CountOf(EntityClass cls) const
{
int n = 0;
for (const EntityDesc& d : entries)
if (d.cls == cls) ++n;
return n;
}
bool EntityCatalog::HasAnyAt(EntityClass cls, int depth) const
{
for (const EntityDesc& d : entries)
if (d.cls == cls && d.InDepth(depth)) return true;
return false;
}
int EntityCatalog::DeepestIntroduction() const
{
int deepest = 1;
for (const EntityDesc& d : entries) deepest = std::max(deepest, d.minDepth);
return deepest;
}
// FindCatalogPath() здесь БОЛЬШЕ НЕТ — по той же причине, что и FindRoomsDir:
// путь к содержимому называет проект (engine/project.h).