592 lines
22 KiB
C++
592 lines
22 KiB
C++
|
|
#include "editor/tools.h"
|
|||
|
|
|
|||
|
|
#include <algorithm>
|
|||
|
|
#include <cstdio>
|
|||
|
|
#include <cstring>
|
|||
|
|
#include <string>
|
|||
|
|
#include <vector>
|
|||
|
|
|
|||
|
|
#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<std::string> 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<std::string> 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<std::pair<std::string, int>> tally;
|
|||
|
|
for (const LevelSpawn& s : lvl.spawns)
|
|||
|
|
{
|
|||
|
|
auto it = std::find_if(tally.begin(), tally.end(),
|
|||
|
|
[&](const std::pair<std::string, int>& 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<std::string, int>& x, const std::pair<std::string, int>& y) {
|
|||
|
|
return x.second > y.second;
|
|||
|
|
});
|
|||
|
|
std::printf("\nнаселение:");
|
|||
|
|
for (const std::pair<std::string, int>& 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<char> 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;
|
|||
|
|
}
|