199 lines
10 KiB
C
199 lines
10 KiB
C
|
|
#pragma once
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Комната — ШАБЛОН, а не готовый кусок карты.
|
|||
|
|
//
|
|||
|
|
// В файле лежит планировка (пол, стены, двери) и точки спавна, но НЕ лежит
|
|||
|
|
// ответ на вопрос «кто здесь стоит». Точка спавна несёт таблицу весов по id
|
|||
|
|
// каталога и шанс срабатывания: одна и та же комната, поставленная дважды,
|
|||
|
|
// населена по-разному. Именно поэтому уровень, собранный из комнат, читается
|
|||
|
|
// как уровень, а не как одна и та же кишка, повторённая пять раз.
|
|||
|
|
//
|
|||
|
|
// Размер ограничен ROOM_MAX_*, потому что генератор ставит комнаты в слоты
|
|||
|
|
// фиксированного размера (engine/level_gen.h). Комната меньше слота — вокруг
|
|||
|
|
// неё остаётся камень, и в нём режутся коридоры; комната размером в слот
|
|||
|
|
// оставила бы коридорам ноль места.
|
|||
|
|
//
|
|||
|
|
// Формат текстовый и построчный: комнаты лежат в git, и правка планировки
|
|||
|
|
// обязана читаться в diff. Бинарный формат сэкономил бы килобайты и отнял бы
|
|||
|
|
// единственный способ увидеть, что именно изменилось.
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
#include <cstdint>
|
|||
|
|
#include <string>
|
|||
|
|
#include <vector>
|
|||
|
|
|
|||
|
|
#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<SpawnOption> 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<SpawnPoint> 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<Room> rooms;
|
|||
|
|
// Комнаты, которые не загрузились: имя файла и причина. Редактор обязан
|
|||
|
|
// показать их списком, а не молча недосчитаться комнаты в наборе.
|
|||
|
|
std::vector<std::string> 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). Движок сам его не ищет.
|