#include "editor/tools.h" #include #include #include #include #include #include "engine/catalog.h" #include "engine/level_gen.h" #include "engine/progression.h" #include "engine/project.h" #include "engine/room.h" #include "engine/tilemap.h" namespace { // --- разбор аргументов ------------------------------------------------------- struct Args { std::string cmd; std::vector rest; // позиционные, без ключей std::string project; const std::string& At(size_t i) const { static const std::string empty; return (i < rest.size()) ? rest[i] : empty; } int Int(size_t i, int fallback) const { const std::string& s = At(i); if (s.empty()) return fallback; return std::atoi(s.c_str()); } }; Args Parse(int argc, char** argv) { Args a; for (int i = 0; i < argc; ++i) { const char* s = argv[i]; if (std::strcmp(s, "--project") == 0 && i + 1 < argc) { a.project = argv[++i]; continue; } if (std::strcmp(s, "--tool") == 0) continue; if (s[0] == '-') continue; if (a.cmd.empty()) a.cmd = s; else a.rest.push_back(s); } return a; } // --- общая часть: поднять проект --------------------------------------------- // // Проект нужен почти каждой команде, и «не выбран» — самая частая ошибка // вызова. Поэтому текст отказа один на все команды и всегда со списком. struct Loaded { Project project; EntityCatalog catalog; RoomLibrary rooms; bool ok = false; }; bool OpenProject(const Args& a, Loaded& out, bool needRooms = true) { std::string why; const std::string dir = FindProjectDir(a.project); if (dir.empty()) { std::printf("ОШИБКА: проект не выбран (не найден или их несколько)\n"); for (const std::string& n : AvailableProjects()) std::printf(" доступен: --project %s\n", n.c_str()); return false; } if (!out.project.Load(dir, &why)) { std::printf("ОШИБКА: %s\n", why.c_str()); return false; } out.project.Apply(); if (!out.catalog.Load(out.project.catalogPath, &why)) { std::printf("ОШИБКА: каталог сущностей: %s\n", why.c_str()); return false; } if (needRooms && !out.rooms.Load(out.project.roomsDir, &why)) { std::printf("ОШИБКА: набор комнат: %s\n", why.c_str()); return false; } out.ok = true; return true; } std::string DepthRange(int minDepth, int maxDepth) { char buf[24]; if (maxDepth > 0) std::snprintf(buf, sizeof(buf), "%d-%d", minDepth, maxDepth); else std::snprintf(buf, sizeof(buf), "%d+", minDepth); return buf; } // --- команды ----------------------------------------------------------------- int CmdHelp() { std::printf( "Tile2D tools — работа с проектом без окна.\n" "\n" " tile2d_editor --tool <команда> [аргументы] [--project <имя>]\n" "\n" "ЧТО ЕСТЬ В ПРОЕКТЕ\n" " projects какие проекты видны отсюда\n" " project манифест: вид, пути, кривая глубины\n" " rooms набор комнат таблицей\n" " room <имя> одна комната: двери, точки спавна, проблемы\n" " ascii <имя> сетка комнаты текстом\n" " catalog [класс] [этаж] что вообще можно ставить, с ценами и глубинами\n" "\n" "ЧТО ИЗ ЭТОГО ПОЛУЧАЕТСЯ\n" " curve <от> <до> во что кривая превращает этажи с <от> по <до>\n" " floor <этаж> [сид] собрать этаж и показать цифры\n" " map <этаж> [сид] собранный этаж картой текстом\n" " validate весь набор: комнаты, покрытие глубин, сборка\n" "\n" "Код возврата 1 означает «не выполнено или найдена поломка» — на это можно\n" "вешать хуки и скрипты, а не читать вывод глазами.\n"); return 0; } int CmdProjects() { const std::vector names = AvailableProjects(); if (names.empty()) { std::printf("проектов не найдено (ожидается projects/<имя>/project.txt)\n"); return 1; } for (const std::string& n : names) { Project p; std::string why; const std::string dir = FindProjectDir(n); if (dir.empty() || !p.Load(dir, &why)) { std::printf("%-12s [СЛОМАН] %s\n", n.c_str(), why.c_str()); continue; } std::printf("%-12s %-8s %-8s %s\n", n.c_str(), p.name.c_str(), ViewModeName(p.view), p.dir.c_str()); } return 0; } int CmdProject(const Args& a) { Loaded L; if (!OpenProject(a, L)) return 1; const Project& p = L.project; const FloorCurve& c = p.curve; std::printf("имя %s\n", p.name.c_str()); std::printf("каталог %s\n", p.dir.c_str()); std::printf("вид %s\n", ViewModeName(p.view)); std::printf("комнаты %s (%d шт.)\n", p.roomsDir.c_str(), L.rooms.Count()); std::printf("сущности %s (%d шт.)\n", p.catalogPath.c_str(), L.catalog.Count()); std::printf("первый этаж %d\n", p.startDepth); std::printf("\nкривая глубины\n"); std::printf(" danger %.2f + %.2f*(d-1) + %.3f*(d-1)^2\n", double(c.dangerBase), double(c.dangerLinear), double(c.dangerQuad)); std::printf(" density 1 + %.3f*(d-1), не выше %.2f\n", double(c.densityPerFloor), double(c.densityCap)); std::printf(" loot %d + d/%d предметов, тир каждые %d этажа\n", c.lootBase, c.floorsPerDrop, c.floorsPerTier); std::printf(" sight 1 - %.3f*(d-1), не ниже %.2f\n", double(c.sightPerFloor), double(c.sightFloorAt)); std::printf(" xp 1 + %.2f*(d-1)\n", double(c.xpPerFloor)); std::printf(" themes "); for (int i = 0; i < THEME_COUNT; ++i) std::printf(" %s c %d", ThemeName(uint8_t(i)), c.themeFrom[i]); std::printf("\n"); return 0; } int CmdRooms(const Args& a) { Loaded L; if (!OpenProject(a, L)) return 1; // Заголовки латиницей: ширина в printf считается в БАЙТАХ, и кириллица // в UTF-8 разъезжается со столбцами ровно вдвое. std::printf("%-14s %6s %5s %6s %6s %7s %6s %s\n", "ROOM", "SIZE", "DOORS", "SPAWNS", "THEME", "DEPTH", "WEIGHT", "STATE"); int bad = 0; for (const Room& r : L.rooms.rooms) { std::string state = RoomProblem(r); if (state.empty() && r.start) state = "start"; if (!RoomProblem(r).empty()) ++bad; std::printf("%-14s %3dx%-2d %5d %6d %6s %7s %6d %s\n", r.name.c_str(), r.w, r.h, r.CountDoors(), int(r.spawns.size()), ThemeName(r.theme), DepthRange(r.minDepth, r.maxDepth).c_str(), r.weight, state.c_str()); } for (const std::string& f : L.rooms.failures) { std::printf("НЕ ЧИТАЕТСЯ %s\n", f.c_str()); ++bad; } return bad > 0 ? 1 : 0; } const Room* FindRoom(const RoomLibrary& lib, const std::string& name) { const int i = lib.IndexOf(name); return (i >= 0) ? &lib.rooms[size_t(i)] : nullptr; } int CmdRoom(const Args& a) { Loaded L; if (!OpenProject(a, L)) return 1; const Room* r = FindRoom(L.rooms, a.At(0)); if (!r) { std::printf("ОШИБКА: нет комнаты `%s`\n", a.At(0).c_str()); return 1; } std::printf("комната %s\n", r->name.c_str()); std::printf("размер %dx%d, дверей %d, вес %d\n", r->w, r->h, r->CountDoors(), r->weight); std::printf("тема %s\n", ThemeName(r->theme)); std::printf("глубина %s%s\n", DepthRange(r->minDepth, r->maxDepth).c_str(), r->start ? ", стартовая" : ""); const std::string problem = RoomProblem(*r); std::printf("состояние %s\n", problem.empty() ? "в порядке" : problem.c_str()); std::printf("\nточки спавна (%d)\n", int(r->spawns.size())); for (const SpawnPoint& p : r->spawns) { std::printf(" (%2d,%2d) шанс %.2f :", p.x, p.y, double(p.chance)); const int total = p.TotalWeight(); for (const SpawnOption& o : p.options) { const int idx = L.catalog.IndexOf(o.id); const double share = (total > 0) ? 100.0 * double(o.weight) / double(total) : 0.0; std::printf(" %s:%d(%.0f%%%s)", o.id.c_str(), o.weight, share, idx < 0 ? ", НЕТ В КАТАЛОГЕ" : ""); } std::printf("\n"); } return problem.empty() ? 0 : 1; } int CmdAscii(const Args& a) { Loaded L; if (!OpenProject(a, L)) return 1; const Room* r = FindRoom(L.rooms, a.At(0)); if (!r) { std::printf("ОШИБКА: нет комнаты `%s`\n", a.At(0).c_str()); return 1; } // Точка спавна рисуется поверх сетки цифрой — её номером в списке. Так по // карте видно и ГДЕ она, и какую строку `spawn` смотреть. std::printf("%s %dx%d\n\n", r->name.c_str(), r->w, r->h); std::printf(" "); for (int x = 0; x < r->w; ++x) std::printf("%d", x % 10); std::printf("\n"); for (int y = 0; y < r->h; ++y) { std::printf("%3d ", y); for (int x = 0; x < r->w; ++x) { char c = '.'; switch (r->At(x, y)) { case RoomCell::WALL: c = '#'; break; case RoomCell::DOOR: c = '+'; break; default: break; } for (size_t i = 0; i < r->spawns.size(); ++i) if (r->spawns[i].x == x && r->spawns[i].y == y) c = char('0' + int(i % 10)); std::printf("%c", c); } std::printf("\n"); } std::printf("\n#стена +дверь .пол цифра — точка спавна (её номер в списке)\n"); return 0; } int CmdCatalog(const Args& a) { Loaded L; if (!OpenProject(a, L, false)) return 1; bool byClass = false; EntityClass want = EntityClass::MONSTER; const std::string& cls = a.At(0); if (cls == "monster") { byClass = true; want = EntityClass::MONSTER; } else if (cls == "prop") { byClass = true; want = EntityClass::PROP; } else if (cls == "loot") { byClass = true; want = EntityClass::LOOT; } else if (!cls.empty()) { std::printf("ОШИБКА: класс бывает monster, prop или loot\n"); return 1; } const int depth = a.Int(1, 0); // 0 — не фильтровать по глубине std::printf("%-12s %-8s %7s %7s %5s %s\n", "ID", "CLASS", "DEPTH", "DANGER", "TIER", "NAME"); int shown = 0; for (int i = 0; i < L.catalog.Count(); ++i) { const EntityDesc& d = L.catalog.At(i); if (byClass && d.cls != want) continue; if (depth > 0 && !(depth >= d.minDepth && (d.maxDepth <= 0 || depth <= d.maxDepth))) continue; std::printf("%-12s %-8s %7s %7.1f %5d %s\n", d.id.c_str(), EntityClassName(d.cls), DepthRange(d.minDepth, d.maxDepth).c_str(), double(d.danger), d.tier, d.name.c_str()); ++shown; } if (depth > 0) std::printf("\nна этаже %d доступно записей: %d\n", depth, shown); return 0; } int CmdCurve(const Args& a) { Loaded L; if (!OpenProject(a, L, false)) return 1; const int from = std::max(1, a.Int(0, 1)); const int to = std::max(from, a.Int(1, from + 11)); std::printf("%-6s %-7s %8s %8s %6s %5s %7s %6s\n", "DEPTH", "THEME", "DANGER", "DENSITY", "LOOT", "TIER", "SIGHT", "XP"); for (int d = from; d <= to; ++d) { const FloorSpec s = DescribeFloor(d); std::printf("%-6d %-7s %8.1f %8.2f %6d %5d %7.2f %6.2f\n", s.depth, ThemeName(s.theme), double(s.dangerBudget), double(s.spawnChanceScale), s.lootDrops, s.lootTier, double(s.sightScale), double(s.xpScale)); } return 0; } // Сборка этажа — общая часть для floor и map. bool BuildFloor(Loaded& L, int depth, uint32_t seed, Tilemap& map, GeneratedLevel& lvl) { const FloorSpec spec = DescribeFloor(depth); std::string why; if (!GenerateLevel(L.rooms, L.catalog, spec, seed, map, lvl, &why)) { std::printf("ОШИБКА: этаж %d сид %u не собрался: %s\n", depth, unsigned(seed), why.c_str()); return false; } return true; } int CmdFloor(const Args& a) { Loaded L; if (!OpenProject(a, L)) return 1; const int depth = std::max(1, a.Int(0, 1)); const uint32_t seed = uint32_t(a.Int(1, 1)); static Tilemap map; static GeneratedLevel lvl; if (!BuildFloor(L, depth, seed, map, lvl)) return 1; std::printf("этаж %d, сид %u, тема %s\n", lvl.floor.depth, unsigned(seed), ThemeName(lvl.floor.theme)); std::printf("комнат %d, коридоров %d, достижимо тайлов %d, до спуска %d шагов\n", lvl.roomsPlaced, lvl.corridors, lvl.reachableTiles, lvl.exitDistance); std::printf("опасность %.1f из %.1f, тварей %d (долив %d), лута %d из тира %d\n", double(lvl.dangerSpent), double(lvl.floor.dangerBudget), lvl.monstersPlaced, lvl.monstersFilled, int(lvl.loot.size()), lvl.floor.lootTier); std::printf("старт (%.1f,%.1f), спуск (%.1f,%.1f), эвакуация (%.1f,%.1f)\n", double(lvl.squadStart.x), double(lvl.squadStart.y), double(lvl.exitPos.x), double(lvl.exitPos.y), double(lvl.extractPos.x), double(lvl.extractPos.y)); // Всё, из-за чего этаж населён не так, как задумано, печатается отдельно: // молчаливая усушка населения — это не «баланс». int bad = 0; if (lvl.spawnsDropped > 0) { std::printf("ПОТЕРИ: срезано потолком %d: %d\n", MAX_LEVEL_SPAWNS, lvl.spawnsDropped); ++bad; } if (lvl.spawnsUnreachable > 0) { std::printf("ПОТЕРИ: недостижимо от старта: %d\n", lvl.spawnsUnreachable); ++bad; } if (lvl.dangerSpent < lvl.floor.dangerBudget * 0.75f) std::printf("ЗАМЕЧАНИЕ: набор отстаёт от кривой, бюджет выбран не полностью\n"); // Кого именно выкатило — по количеству, а не списком: список из полусотни // строк невозможно сравнить с таким же списком другого сида. std::vector> tally; for (const LevelSpawn& s : lvl.spawns) { auto it = std::find_if(tally.begin(), tally.end(), [&](const std::pair& p) { return p.first == s.id; }); if (it == tally.end()) tally.push_back({s.id, 1}); else ++it->second; } std::sort(tally.begin(), tally.end(), [](const std::pair& x, const std::pair& y) { return x.second > y.second; }); std::printf("\nнаселение:"); for (const std::pair& p : tally) std::printf(" %s x%d", p.first.c_str(), p.second); std::printf("\nлут: "); for (const LevelSpawn& s : lvl.loot) std::printf(" %s", s.id.c_str()); std::printf("\n"); return bad > 0 ? 1 : 0; } int CmdMap(const Args& a) { Loaded L; if (!OpenProject(a, L)) return 1; const int depth = std::max(1, a.Int(0, 1)); const uint32_t seed = uint32_t(a.Int(1, 1)); static Tilemap map; static GeneratedLevel lvl; if (!BuildFloor(L, depth, seed, map, lvl)) return 1; // Карта строится в буфер символов, потом на неё кладутся сущности: иначе // тварь, стоящая в дверях, потерялась бы под символом двери. std::vector grid(size_t(MAP_W) * size_t(MAP_H), ' '); for (int y = 0; y < MAP_H; ++y) for (int x = 0; x < MAP_W; ++x) grid[size_t(y) * MAP_W + x] = map.IsWall(x, y) ? '#' : '.'; auto put = [&](Vec2 p, char c) { const int x = int(p.x); const int y = int(p.y); if (Tilemap::InBounds(x, y)) grid[size_t(y) * MAP_W + x] = c; }; for (const LevelSpawn& s : lvl.spawns) { const EntityDesc* d = L.catalog.Find(s.id); put(s.pos, (d && d->cls == EntityClass::PROP) ? 'o' : 'm'); } for (const LevelSpawn& s : lvl.loot) put(s.pos, '$'); put(lvl.exitPos, '>'); put(lvl.extractPos, '<'); put(lvl.squadStart, '@'); std::printf("этаж %d, сид %u, тема %s, комнат %d\n\n", lvl.floor.depth, unsigned(seed), ThemeName(lvl.floor.theme), lvl.roomsPlaced); for (int y = 0; y < MAP_H; ++y) { std::printf("%3d ", y); for (int x = 0; x < MAP_W; ++x) std::printf("%c", grid[size_t(y) * MAP_W + x]); std::printf("\n"); } std::printf("\n# стена . пол @ старт > спуск < эвакуация m тварь o обстановка $ лут\n"); return 0; } int CmdValidate(const Args& a) { Loaded L; if (!OpenProject(a, L)) return 1; int bad = 0; int warn = 0; for (const Room& r : L.rooms.rooms) { const std::string problem = RoomProblem(r); if (!problem.empty()) { std::printf("СЛОМАНО %s: %s\n", r.name.c_str(), problem.c_str()); ++bad; } for (const SpawnPoint& p : r.spawns) for (const SpawnOption& o : p.options) if (L.catalog.IndexOf(o.id) < 0) { std::printf("СЛОМАНО %s (%d,%d): id `%s` нет в каталоге\n", r.name.c_str(), p.x, p.y, o.id.c_str()); ++bad; } } for (const std::string& f : L.rooms.failures) { std::printf("СЛОМАНО не читается %s\n", f.c_str()); ++bad; } if (!L.rooms.HasStartRoom()) { std::printf("СЛОМАНО в наборе нет ни одной стартовой комнаты\n"); ++bad; } // Покрытие глубин: главный вопрос бесконечного спуска — не «хороша ли // комната», а «есть ли чем застроить сотый этаж». const int probeMax = std::max(L.catalog.DeepestIntroduction(), 12) + 4; for (int d = 1; d <= probeMax; ++d) { const bool roomsHere = L.rooms.CountAt(d) > 0; const bool startHere = L.rooms.HasStartRoomAt(d); const bool monsters = L.catalog.HasAnyAt(EntityClass::MONSTER, d); const bool loot = L.catalog.HasAnyAt(EntityClass::LOOT, d); if (roomsHere && startHere && monsters && loot) continue; std::printf("СЛОМАНО этаж %d:%s%s%s%s\n", d, roomsHere ? "" : " нет комнат", startHere ? "" : " нет стартовой", monsters ? "" : " нет тварей", loot ? "" : " нет лута"); ++bad; } static Tilemap map; static GeneratedLevel lvl; for (const int d : {1, 3, 6, 10, 16, 24}) for (const uint32_t seed : {1u, 42u}) { const FloorSpec spec = DescribeFloor(d); std::string why; if (!GenerateLevel(L.rooms, L.catalog, spec, seed, map, lvl, &why)) { std::printf("СЛОМАНО этаж %d сид %u: %s\n", d, unsigned(seed), why.c_str()); ++bad; continue; } if (lvl.spawnsUnreachable > 0 || lvl.spawnsDropped > 0 || lvl.loot.empty() || lvl.exitDistance <= 0) { std::printf("СЛОМАНО этаж %d сид %u: недостижимых %d, срезано %d, лута %d, " "до спуска %d\n", d, unsigned(seed), lvl.spawnsUnreachable, lvl.spawnsDropped, int(lvl.loot.size()), lvl.exitDistance); ++bad; } else if (lvl.dangerSpent < spec.dangerBudget * 0.75f) { std::printf("ЗАМЕЧАНИЕ этаж %d сид %u: опасность %.0f из %.0f\n", d, unsigned(seed), double(lvl.dangerSpent), double(spec.dangerBudget)); ++warn; } } std::printf("\n%s: комнат %d, сущностей %d, поломок %d, замечаний %d\n", L.project.name.c_str(), L.rooms.Count(), L.catalog.Count(), bad, warn); return bad > 0 ? 1 : 0; } } // namespace int RunTool(int argc, char** argv) { const Args a = Parse(argc, argv); if (a.cmd.empty() || a.cmd == "help") return CmdHelp(); if (a.cmd == "projects") return CmdProjects(); if (a.cmd == "project") return CmdProject(a); if (a.cmd == "rooms") return CmdRooms(a); if (a.cmd == "room") return CmdRoom(a); if (a.cmd == "ascii") return CmdAscii(a); if (a.cmd == "catalog") return CmdCatalog(a); if (a.cmd == "curve") return CmdCurve(a); if (a.cmd == "floor") return CmdFloor(a); if (a.cmd == "map") return CmdMap(a); if (a.cmd == "validate") return CmdValidate(a); std::printf("ОШИБКА: нет команды `%s`\n\n", a.cmd.c_str()); CmdHelp(); return 1; }