#pragma once // ----------------------------------------------------------------------------- // Сборка ЭТАЖА из комнат. Детерминирована по сиду и глубине, и только по ним: // один и тот же набор комнат плюс тот же сид и тот же этаж дают побайтово ту же // карту и то же население. // // Где живёт случайность. В симуляции её нет и не будет (docs/02-architecture.md, // «Детерминизм»): RNG крутится ЗДЕСЬ, один раз, при сборке этажа, и к моменту // первого Game::Step всё уже разложено. Поэтому прогон остаётся воспроизводимым, // а `--accept` вообще не проходит по этому коду — приёмка меряется на прежней // захардкоженной планировке. // // Раскладка. Карта 48x48 делится на сетку LEVEL_GRID x LEVEL_GRID слотов по // ROOM_SLOT тайлов. В каждый слот ставится комната (не больше ROOM_MAX_*), она // центрируется, и вокруг неё остаётся камень. Слоты сшиваются коридорами: сперва // остовное дерево — оно гарантирует, что дойти можно до каждой комнаты, — потом // несколько лишних связей, чтобы этаж не был деревом-кишкой. // // Что делает глубина. Всё, кроме планировки: // * из набора отбираются комнаты, разрешённые на этом этаже; // * из таблиц спавна отбираются сущности, разрешённые на этом этаже; // * население тратит БЮДЖЕТ ОПАСНОСТИ, а не просто выпадает по шансу; // * остаток бюджета доливается тварями вне комнатных точек — иначе глубина // упиралась бы в число точек, нарисованных в комнатах, и переставала расти; // * лут раскладывается тиром этажа. // Сама кривая — в engine/progression.h, здесь только её применение. // ----------------------------------------------------------------------------- #include #include #include #include "engine/math.h" #include "engine/catalog.h" #include "engine/progression.h" #include "engine/room.h" #include "engine/tilemap.h" constexpr int LEVEL_GRID = MAP_W / ROOM_SLOT; constexpr int LEVEL_SLOTS = LEVEL_GRID * LEVEL_GRID; // Потолок на лежащие предметы. Не «на всякий случай»: каждый предмет — // сущность ECS и иконка в списке отрисовки, а этаж, засыпанный лутом, // перестаёт быть находкой. constexpr int MAX_LEVEL_LOOT = 24; // Потолок на населённость этажа. Раньше здесь стояла игровая константа // MAX_TARGETS из src/tuning.h — то есть генератор уровней знал, сколько битов // в маске удара клинком. Теперь у движка своё число, а игра проверяет, что оно // влезает в её пределы (static_assert в src/tuning.h). // // Это не формальность: бюджет опасности на глубине неизбежно перерастает любое // такое число, и лишнее не теряется молча — оно уходит в GeneratedLevel:: // spawnsDropped и в отчёт --check. constexpr int MAX_LEVEL_SPAWNS = 64; static_assert(MAP_W == MAP_H, "генератор рассчитан на квадратную карту"); static_assert(MAP_W % ROOM_SLOT == 0, "карта обязана делиться на слоты нацело"); // ----------------------------------------------------------------------------- // Xorshift32. Свой, а не : стандартные распределения не обязаны давать // одинаковую последовательность на разных реализациях библиотеки, а сид этажа // обязан значить одно и то же везде. // ----------------------------------------------------------------------------- struct LevelRng { uint32_t state; explicit LevelRng(uint32_t seed) : state(seed ? seed : 0x9E3779B9u) {} uint32_t Next() { state ^= state << 13; state ^= state >> 17; state ^= state << 5; return state; } // [0, 1). Двадцать четыре бита: больше во float всё равно не влезает. float Float01() { return float(Next() >> 8) * (1.0f / 16777216.0f); } // [0, n). n <= 0 — ноль: вызывающему не приходится проверять пустой список. int Below(int n) { return (n > 0) ? int(Next() % uint32_t(n)) : 0; } }; // Одна сущность, которую генератор выкатил в мир. id — строка каталога: // движок не знает, во что она превратится в игре. struct LevelSpawn { std::string id; Vec2 pos{}; }; struct LevelSlot { int roomIndex = -1; // индекс в RoomLibrary; -1 — слот пуст int originX = 0; // угол комнаты в тайлах карты int originY = 0; }; struct GeneratedLevel { uint32_t seed = 0; FloorSpec floor; Vec2 squadStart{}; // Спуск на следующий этаж. Самая дальняя от старта ДОСТИЖИМАЯ точка: этаж // обязан заставить пройти себя, а не открываться в двух шагах от входа. Vec2 exitPos{}; // Эвакуация: та же точка, откуда отряд вошёл на этаж, — лестница наверх. // Отдельным полем, а не «это же squadStart», потому что смысл у них разный // и разойтись они могут: точку входа однажды захочется двигать, а выход // наверх — нет. // // Совпадение входа и выхода — не экономия, а суть напряжения: спуск лежит // на дальнем конце этажа, и решив уйти, отряд идёт обратно через всё, что // успел растревожить, — раненый и с полными руками. Vec2 extractPos{}; std::vector spawns; // твари и обстановка std::vector loot; // предметы на полу LevelSlot slots[LEVEL_SLOTS]; int roomsPlaced = 0; int corridors = 0; // Сколько опасности реально выпущено на этаж. Не равно бюджету: точек может // не хватить, и это нормально. Сильно меньше бюджета на глубине — признак, // что набор комнат перестал успевать за кривой. float dangerSpent = 0.0f; int monstersPlaced = 0; // Сколько тварей долито вне комнатных точек, чтобы выбрать бюджет. int monstersFilled = 0; // Сколько точек спавна выпало из-за потолка MAX_LEVEL_SPAWNS. Не ноль — этаж // населён не так, как задумано, и это обязано быть видно, а не молча съедено. int spawnsDropped = 0; // Сколько выпало как недостижимые от точки старта. Не ноль — где-то в наборе // комнат есть карман, отрезанный от собственной двери: тварь в нём не сможет // ни дойти до отряда, ни быть убитой, и этаж тихо станет легче. int spawnsUnreachable = 0; // Сколько тайлов пола достижимо от старта, и как далеко до спуска (в шагах // по сетке). Обе цифры — про то, «есть ли на этаже что проходить». int reachableTiles = 0; int exitDistance = 0; const LevelSlot& Slot(int gx, int gy) const { return slots[gy * LEVEL_GRID + gx]; } }; // Собирает планировку прямо в map (включая слой тем) и население в out. // false — из набора нельзя собрать этаж; map при этом не трогается. // // catalog нужен, потому что глубина и цена сущности живут в нём: без каталога // генератор не может ни отобрать тварей по этажу, ни посчитать бюджет. bool GenerateLevel(const RoomLibrary& lib, const EntityCatalog& catalog, const FloorSpec& floor, uint32_t seed, Tilemap& map, GeneratedLevel& out, std::string* err = nullptr);