#pragma once // ----------------------------------------------------------------------------- // Комната — ШАБЛОН, а не готовый кусок карты. // // В файле лежит планировка (пол, стены, двери) и точки спавна, но НЕ лежит // ответ на вопрос «кто здесь стоит». Точка спавна несёт таблицу весов по id // каталога и шанс срабатывания: одна и та же комната, поставленная дважды, // населена по-разному. Именно поэтому уровень, собранный из комнат, читается // как уровень, а не как одна и та же кишка, повторённая пять раз. // // Размер ограничен ROOM_MAX_*, потому что генератор ставит комнаты в слоты // фиксированного размера (engine/level_gen.h). Комната меньше слота — вокруг // неё остаётся камень, и в нём режутся коридоры; комната размером в слот // оставила бы коридорам ноль места. // // Формат текстовый и построчный: комнаты лежат в git, и правка планировки // обязана читаться в diff. Бинарный формат сэкономил бы килобайты и отнял бы // единственный способ увидеть, что именно изменилось. // ----------------------------------------------------------------------------- #include #include #include #include "engine/theme.h" // Слот генератора — 16 тайлов (48/3). Комната не может занять его целиком: // хотя бы по тайлу с каждой стороны остаётся под коридоры и рамку. constexpr int ROOM_SLOT = 16; constexpr int ROOM_MAX_W = 14; constexpr int ROOM_MAX_H = 14; constexpr int ROOM_MIN_W = 5; constexpr int ROOM_MIN_H = 5; enum class RoomCell : uint8_t { EMPTY = 0, // пол WALL = 1, DOOR = 2, // проход наружу: пол, к которому генератор тянет коридор }; // Стороны в мировых осях: y растёт на юг, x — на восток. enum class RoomSide : uint8_t { NORTH = 0, EAST, SOUTH, WEST, COUNT, }; inline int SideDX(RoomSide s) { return (s == RoomSide::EAST) ? 1 : (s == RoomSide::WEST ? -1 : 0); } inline int SideDY(RoomSide s) { return (s == RoomSide::SOUTH) ? 1 : (s == RoomSide::NORTH ? -1 : 0); } inline RoomSide OppositeSide(RoomSide s) { return RoomSide((int(s) + 2) % 4); } // Один вариант в таблице точки спавна. id — строка каталога, а не индекс: // каталог правится руками, и перестановка строк в нём не должна молча менять // население всех комнат разом. struct SpawnOption { std::string id; int weight = 1; }; // Точка спавна: место плюс таблица «кто здесь может оказаться». struct SpawnPoint { int x = 0; int y = 0; float chance = 1.0f; // вероятность, что точка вообще сработает std::vector options; // пусто — точка не даёт никого int TotalWeight() const; // Выбрать вариант по броску [0..1). -1 — таблица пуста или все веса нулевые. int Pick(float roll01) const; // Вес по id; 0 — такого варианта в таблице нет. int WeightOf(const std::string& id) const; // Выставить вес. 0 убирает вариант из таблицы: нулевые веса в файле — // это мусор, который переживает правку и потом путает. void SetWeight(const std::string& id, int weight); }; struct Room { std::string name; // имя файла без расширения int w = 12; int h = 10; int weight = 1; // вес комнаты при выборе генератором uint8_t theme = 0; bool start = false; // годится как стартовая комната отряда // --- глубина -------------------------------------------------------------- // С какого этажа комната встречается и по какой (0 — до бесконечности). // Планировка — тоже часть «чем глубже, тем хуже»: широкая арена первых // этажей и теснота, где нельзя развернуть строй, — это разные игры, и // отдавать их случайности на всех этажах поровну незачем. int minDepth = 1; int maxDepth = 0; bool InDepth(int depth) const { return depth >= minDepth && (maxDepth <= 0 || depth <= maxDepth); } RoomCell cells[ROOM_MAX_W * ROOM_MAX_H] = {}; std::vector spawns; Room(); // пустая комната с рамкой стен static bool SizeOk(int w, int h) { return w >= ROOM_MIN_W && w <= ROOM_MAX_W && h >= ROOM_MIN_H && h <= ROOM_MAX_H; } bool InBounds(int x, int y) const { return unsigned(x) < unsigned(w) && unsigned(y) < unsigned(h); } // Вне комнаты — стена. То же правило, что у Tilemap: оно убирает граничные // проверки из всего, что ходит по сетке. RoomCell At(int x, int y) const { return InBounds(x, y) ? cells[size_t(y) * ROOM_MAX_W + x] : RoomCell::WALL; } void Set(int x, int y, RoomCell c) { if (InBounds(x, y)) cells[size_t(y) * ROOM_MAX_W + x] = c; } bool Walkable(int x, int y) const { return At(x, y) != RoomCell::WALL; } bool OnBorder(int x, int y) const { return InBounds(x, y) && (x == 0 || y == 0 || x == w - 1 || y == h - 1); } // --- точки спавна --------------------------------------------------------- int SpawnIndexAt(int x, int y) const; SpawnPoint* SpawnAt(int x, int y); const SpawnPoint* SpawnAt(int x, int y) const; // Точка на этом месте либо уже есть, либо заводится пустой. SpawnPoint& TouchSpawnAt(int x, int y); void RemoveSpawnAt(int x, int y); // --- правка планировки ---------------------------------------------------- // Размер меняется с сохранением содержимого; всё, что вышло за новую // границу, теряется вместе с точками спавна на нём. void Resize(int newW, int newH); void FillBorder(); // обвести рамкой стен, двери сохранить void ClearInterior(); // выскоблить всё внутри рамки // --- двери ---------------------------------------------------------------- // Двери обязаны стоять на рамке: генератор тянет коридор НАРУЖУ, и дверь // посреди комнаты вела бы в собственную стену. int CountDoors() const; int CountDoorsOn(RoomSide side) const; // Первая дверь на стороне. false — на этой стороне дверей нет. bool FirstDoorOn(RoomSide side, int& outX, int& outY) const; // Куда генератор пробьёт стену, если дверей на стороне нет: середина стороны. void DefaultBreachOn(RoomSide side, int& outX, int& outY) const; }; // Что в комнате не так. Пустая строка — всё в порядке. Не «загрузилась ли», // а «сыграет ли»: комнату без дверей генератор поставить не сможет. std::string RoomProblem(const Room& r); bool LoadRoom(const std::string& path, Room& out, std::string* err = nullptr); bool SaveRoom(const std::string& path, const Room& r, std::string* err = nullptr); // ----------------------------------------------------------------------------- // Набор комнат: всё, что лежит в assets/rooms/*.room. // ----------------------------------------------------------------------------- struct RoomLibrary { std::string dir; std::vector rooms; // Комнаты, которые не загрузились: имя файла и причина. Редактор обязан // показать их списком, а не молча недосчитаться комнаты в наборе. std::vector failures; // Порядок — алфавитный по имени файла: генератор детерминирован по сиду, // а порядок обхода каталога файловой системой таковым не является. bool Load(const std::string& dirPath, std::string* err = nullptr); int Count() const { return int(rooms.size()); } int TotalWeight() const; int IndexOf(const std::string& name) const; bool HasStartRoom() const; // Есть ли чем застроить этот этаж. Разделено на два вопроса, потому что // это две РАЗНЫЕ поломки забега: «нет обычных комнат» и «нет стартовой». int CountAt(int depth) const; bool HasStartRoomAt(int depth) const; }; // Где лежит набор — знает проект (engine/project.h). Движок сам его не ищет.