156 lines
9.6 KiB
C
156 lines
9.6 KiB
C
|
|
#pragma once
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Сборка ЭТАЖА из комнат. Детерминирована по сиду и глубине, и только по ним:
|
|||
|
|
// один и тот же набор комнат плюс тот же сид и тот же этаж дают побайтово ту же
|
|||
|
|
// карту и то же население.
|
|||
|
|
//
|
|||
|
|
// Где живёт случайность. В симуляции её нет и не будет (docs/02-architecture.md,
|
|||
|
|
// «Детерминизм»): RNG крутится ЗДЕСЬ, один раз, при сборке этажа, и к моменту
|
|||
|
|
// первого Game::Step всё уже разложено. Поэтому прогон остаётся воспроизводимым,
|
|||
|
|
// а `--accept` вообще не проходит по этому коду — приёмка меряется на прежней
|
|||
|
|
// захардкоженной планировке.
|
|||
|
|
//
|
|||
|
|
// Раскладка. Карта 48x48 делится на сетку LEVEL_GRID x LEVEL_GRID слотов по
|
|||
|
|
// ROOM_SLOT тайлов. В каждый слот ставится комната (не больше ROOM_MAX_*), она
|
|||
|
|
// центрируется, и вокруг неё остаётся камень. Слоты сшиваются коридорами: сперва
|
|||
|
|
// остовное дерево — оно гарантирует, что дойти можно до каждой комнаты, — потом
|
|||
|
|
// несколько лишних связей, чтобы этаж не был деревом-кишкой.
|
|||
|
|
//
|
|||
|
|
// Что делает глубина. Всё, кроме планировки:
|
|||
|
|
// * из набора отбираются комнаты, разрешённые на этом этаже;
|
|||
|
|
// * из таблиц спавна отбираются сущности, разрешённые на этом этаже;
|
|||
|
|
// * население тратит БЮДЖЕТ ОПАСНОСТИ, а не просто выпадает по шансу;
|
|||
|
|
// * остаток бюджета доливается тварями вне комнатных точек — иначе глубина
|
|||
|
|
// упиралась бы в число точек, нарисованных в комнатах, и переставала расти;
|
|||
|
|
// * лут раскладывается тиром этажа.
|
|||
|
|
// Сама кривая — в engine/progression.h, здесь только её применение.
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
#include <cstdint>
|
|||
|
|
#include <string>
|
|||
|
|
#include <vector>
|
|||
|
|
|
|||
|
|
#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. Свой, а не <random>: стандартные распределения не обязаны давать
|
|||
|
|
// одинаковую последовательность на разных реализациях библиотеки, а сид этажа
|
|||
|
|
// обязан значить одно и то же везде.
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
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<LevelSpawn> spawns; // твари и обстановка
|
|||
|
|
std::vector<LevelSpawn> 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);
|