载入中...
搜索中...
未找到
RoguelikeGenerator.cpp
浏览该文件的文档.
1// RoguelikeGenerator �?a configurable, seed-driven room-and-corridor level
2// generator ("RoguelikeGenerator Pro" style). Unlike dungeon.bsp it layers
3// extra data on top of the wall/floor grid:
4// * `detail` (Grid2D) stores per-cell wall-autotile direction masks, floor
5// pattern variants and scattered decor tiles (see k* constants below).
6// * `objects` carry placed props (pillars / chests / grass tufts) and the
7// spawn / stairs markers.
8// Everything is driven by Params so scripts can re-roll with a different seed
9// or tweak generation rules without touching code. Generated cells use the
10// standard Semantic ids, so the result still applies to a TileLayer through
11// the usual palette pipeline.
12//
13// Register id: "level.roguelike"
15
17#include "procgen/Grid2D.h"
18#include "procgen/Params.h"
19#include "procgen/Semantic.h"
20
21#include <algorithm>
22#include <cmath>
23#include <cstdint>
24#include <limits>
25#include <random>
26#include <sstream>
27#include <string>
28#include <unordered_set>
29#include <vector>
30
31namespace eve::procgen {
32namespace {
33
34// ---- detail-layer encoding (documented; the script example consumes these) ----
35// Wall cells: detail = 8-bit neighbour mask (which directions are walkable).
36// Floor cells: detail = floor-pattern variant (1..kFloorMaxVariant), or
37// >= kDecorBase to mark a scattered decor tile.
38constexpr int kDecorBase = 100; // floor detail >= this => decor tile
39constexpr int kFloorMaxVariant = 32; // floor pattern variant cap
40
41// Neighbour direction bits used by the wall mask.
42constexpr int kE = 1 << 0; // +x
43constexpr int kS = 1 << 1; // +y
44constexpr int kW = 1 << 2; // -x
45constexpr int kN = 1 << 3; // -y
46constexpr int kSE = 1 << 4;
47constexpr int kSW = 1 << 5;
48constexpr int kNW = 1 << 6;
49constexpr int kNE = 1 << 7;
50
51bool isWalkable(uint32_t c) {
52 return c == Semantic::Floor || c == Semantic::Corridor;
53}
54
55struct Rect {
56 int x = 0;
57 int y = 0;
58 int w = 0;
59 int h = 0;
60};
61
62int clampInt(int v, int lo, int hi) { return std::max(lo, std::min(hi, v)); }
63
64std::vector<std::string> assetPool(const Params &params, const std::string &role,
65 const std::string &fallback) {
66 std::vector<std::string> result;
67 std::stringstream input(params.getString("assets." + role, fallback));
68 std::string item;
69 while (std::getline(input, item, ',')) {
70 const auto first = item.find_first_not_of(" \t");
71 const auto last = item.find_last_not_of(" \t");
72 if (first != std::string::npos) result.push_back(item.substr(first, last - first + 1));
73 }
74 return result;
75}
76
77std::string pickAsset(const std::vector<std::string> &pool, std::mt19937 &rng) {
78 if (pool.empty()) return {};
79 return pool[size_t(rng()) % pool.size()];
80}
81
83void autotileWalls(Grid2D &out) {
84 const int w = out.getWidth();
85 const int h = out.getHeight();
86 for (int y = 0; y < h; ++y) {
87 for (int x = 0; x < w; ++x) {
88 if (uint32_t(out.getCell(x, y)) != Semantic::Wall) continue;
89 int mask = 0;
90 if (x + 1 < w && isWalkable(uint32_t(out.getCell(x + 1, y)))) mask |= kE;
91 if (y + 1 < h && isWalkable(uint32_t(out.getCell(x, y + 1)))) mask |= kS;
92 if (x - 1 >= 0 && isWalkable(uint32_t(out.getCell(x - 1, y)))) mask |= kW;
93 if (y - 1 >= 0 && isWalkable(uint32_t(out.getCell(x, y - 1)))) mask |= kN;
94 if (x + 1 < w && y + 1 < h && isWalkable(uint32_t(out.getCell(x + 1, y + 1))))
95 mask |= kSE;
96 if (x - 1 >= 0 && y + 1 < h && isWalkable(uint32_t(out.getCell(x - 1, y + 1))))
97 mask |= kSW;
98 if (x - 1 >= 0 && y - 1 >= 0 && isWalkable(uint32_t(out.getCell(x - 1, y - 1))))
99 mask |= kNW;
100 if (x + 1 < w && y - 1 >= 0 && isWalkable(uint32_t(out.getCell(x + 1, y - 1))))
101 mask |= kNE;
102 out.setDetail(x, y, mask);
103 }
104 }
105}
106
108int floorVariant(int x, int y, const std::string &pattern, int variants, uint32_t salt) {
109 const int v = std::max(1, variants);
110 switch (pattern[0]) {
111 case 'c': // checker
112 return 1 + ((x + y) % v);
113 case 'p': // plank
114 return 1 + (y % v);
115 default: // brick / plain / cobble -> pseudo-random but stable per seed
116 return 1 + (int((x * 73856093u + y * 19349663u + salt) & 0x7FFFFFFFu) % v);
117 }
118}
119
120void carveRect(Grid2D &out, const Rect &r, uint32_t semantic) {
121 for (int y = r.y; y < r.y + r.h; ++y)
122 for (int x = r.x; x < r.x + r.w; ++x) out.setCell(x, y, int(semantic));
123}
124
125bool rectsOverlap(const Rect &a, const Rect &b, int pad) {
126 return a.x < b.x + b.w + pad && b.x < a.x + a.w + pad && a.y < b.y + b.h + pad &&
127 b.y < a.y + a.h + pad;
128}
129
130void carveCorridor(Grid2D &out, int ax, int ay, int bx, int by, int width,
131 const std::string &style, std::mt19937 &rng) {
132 auto carve = [&](int x, int y) {
133 if (uint32_t(out.getCell(x, y)) == Semantic::Wall)
134 out.setCell(x, y, int(Semantic::Corridor));
135 };
136 if (style == "diagonal") {
137 int x = ax;
138 int y = ay;
139 while (x != bx || y != by) {
140 for (int oy = 0; oy < width; ++oy)
141 for (int ox = 0; ox < width; ++ox)
142 carve(x + ox, y + oy);
143 if (x != bx && y != by && (rng() & 1u)) {
144 x += (bx > x) ? 1 : -1;
145 } else if (x != bx) {
146 x += (bx > x) ? 1 : -1;
147 } else {
148 y += (by > y) ? 1 : -1;
149 }
150 }
151 for (int oy = 0; oy < width; ++oy)
152 for (int ox = 0; ox < width; ++ox) carve(x + ox, y + oy);
153 return;
154 }
155
156 // L-shaped: horizontal then vertical (random bend for variety on "l").
157 int cornerX = bx;
158 int cornerY = ay;
159 if (style == "straight") {
160 // Force a straight line when aligned, otherwise keep the L.
161 } else if (rng() & 1u) {
162 cornerX = ax;
163 cornerY = by;
164 }
165
166 const int x0 = std::min(ax, cornerX);
167 const int x1 = std::max(ax, cornerX);
168 for (int x = x0; x <= x1; ++x)
169 for (int oy = 0; oy < width; ++oy)
170 carve(x, cornerY + oy);
171
172 const int y0 = std::min(cornerY, by);
173 const int y1 = std::max(cornerY, by);
174 for (int y = y0; y <= y1; ++y)
175 for (int ox = 0; ox < width; ++ox)
176 carve(cornerX + ox, y);
177}
178
179bool genRoguelike(const Params &params, Grid2D &out, std::string &error) {
180 const int w = params.getWidth();
181 const int h = params.getHeight();
182 if (w < 9 || h < 9) {
183 error = "level.roguelike: size must be at least 9x9";
184 return false;
185 }
186
187 const uint32_t seed = params.getSeed();
188 const int roomCount = clampInt(params.getInt("roomCount", 9), 1, 256);
189 const int roomMin = clampInt(params.getInt("roomMin", 4), 2, w);
190 const int roomMax = clampInt(params.getInt("roomMax", 8), roomMin, w);
191 const int padding = clampInt(params.getInt("padding", 1), 0, 4);
192 const int spacing = clampInt(params.getInt("spacing", 2), 0, 8);
193 const int clusterGapMin = clampInt(params.getInt("clusterGapMin", spacing + 1), 1, 8);
194 const int clusterGapMax = clampInt(params.getInt("clusterGapMax", spacing + 3),
195 clusterGapMin, 12);
196 const int clusterBranchBias = clampInt(params.getInt("clusterBranchBias", 0), 0, 4);
197 const int corridorW = clampInt(params.getInt("corridorWidth", 1), 1, 3);
198 const int stairCount = clampInt(params.getInt("stairCount", 1), 0, 4);
199 const int stairSideMask = clampInt(params.getInt("stairSideMask", 15), 1, 15);
200 const std::string layout = params.getString("layoutStyle", "grid");
201 const std::string style = params.getString("corridorStyle", "l");
202 const std::string connections = params.getString("connectionStyle", "sequential");
203 const std::string pattern = params.getString("floorPattern", "brick");
204 const int variants = clampInt(params.getInt("floorVariants", 4), 1, kFloorMaxVariant);
205 const float decorDensity = std::clamp(params.getFloat("decorDensity", 0.05f), 0.f, 1.f);
206 const std::string decorSet = params.getString("decorSet", "mixed");
207 const bool doAutotile = params.getInt("autotile", 1) != 0;
208
209 out.resize(w, h);
210 out.fill(Semantic::Wall);
211
212 std::mt19937 rng(seed);
213
214 // 1) Place rooms on a coarse grid partition so they stay spread out, but
215 // allow jitter within each slot. Reject a slot if its room would overlap
216 // an earlier one (with spacing) or push outside the map.
217 const int cols = std::max(1, int(std::sqrt(double(roomCount))));
218 const int rows = std::max(1, (roomCount + cols - 1) / cols);
219 std::vector<Rect> rooms;
220 std::vector<size_t> roomParents;
221 std::uniform_int_distribution<int> dim(roomMin, roomMax);
222 std::uniform_int_distribution<int> jx(0, 0), jy(0, 0);
223
224 if (layout == "clustered") {
225 const int firstW = std::min(dim(rng), w - padding * 2);
226 const int firstH = std::min(dim(rng), h - padding * 2);
227 rooms.push_back({(w - firstW) / 2, (h - firstH) / 2, firstW, firstH});
228 roomParents.push_back(0);
229 std::uniform_int_distribution<int> jitter(-1, 1);
230 std::uniform_int_distribution<int> clusterGap(clusterGapMin, clusterGapMax);
231 const int maxAttempts = roomCount * 160;
232 for (int attempt = 0; attempt < maxAttempts && int(rooms.size()) < roomCount; ++attempt) {
233 size_t parentIndex = size_t(rng()) % rooms.size();
234 // Sampling additional candidates and keeping the oldest biases
235 // growth toward established hubs without prescribing a layout.
236 // Zero preserves uniform parent selection.
237 for (int sample = 0; sample < clusterBranchBias; ++sample)
238 parentIndex = std::min(parentIndex, size_t(rng()) % rooms.size());
239 const Rect &parent = rooms[parentIndex];
240 const int rw = std::min(dim(rng), w - padding * 2);
241 const int rh = std::min(dim(rng), h - padding * 2);
242 const int gap = clusterGap(rng);
243 Rect cand{0, 0, rw, rh};
244 switch (rng() % 4u) {
245 case 0: // east
246 cand.x = parent.x + parent.w + gap;
247 cand.y = parent.y + parent.h / 2 - rh / 2 + jitter(rng);
248 break;
249 case 1: // west
250 cand.x = parent.x - rw - gap;
251 cand.y = parent.y + parent.h / 2 - rh / 2 + jitter(rng);
252 break;
253 case 2: // south
254 cand.x = parent.x + parent.w / 2 - rw / 2 + jitter(rng);
255 cand.y = parent.y + parent.h + gap;
256 break;
257 default: // north
258 cand.x = parent.x + parent.w / 2 - rw / 2 + jitter(rng);
259 cand.y = parent.y - rh - gap;
260 break;
261 }
262 if (cand.x < padding || cand.y < padding ||
263 cand.x + cand.w > w - padding || cand.y + cand.h > h - padding) continue;
264 bool ok = true;
265 for (const Rect &other : rooms) {
266 if (rectsOverlap(cand, other, spacing)) { ok = false; break; }
267 }
268 if (ok) {
269 rooms.push_back(cand);
270 roomParents.push_back(parentIndex);
271 }
272 }
273 } else {
274 for (int r = 0; r < rows; ++r) {
275 for (int c = 0; c < cols; ++c) {
276 if (int(rooms.size()) >= roomCount) break;
277 const int sx = padding + int((long(c) * (w - 2 * padding)) / cols);
278 const int ex = padding + int((long(c + 1) * (w - 2 * padding)) / cols);
279 const int sy = padding + int((long(r) * (h - 2 * padding)) / rows);
280 const int ey = padding + int((long(r + 1) * (h - 2 * padding)) / rows);
281 const int slotW = std::max(roomMin, ex - sx);
282 const int slotH = std::max(roomMin, ey - sy);
283 if (slotW < roomMin || slotH < roomMin) continue;
284 for (int attempt = 0; attempt < 24; ++attempt) {
285 const int rw = std::min(dim(rng), slotW);
286 const int rh = std::min(dim(rng), slotH);
287 std::uniform_int_distribution<int> ox(sx, sx + slotW - rw);
288 std::uniform_int_distribution<int> oy(sy, sy + slotH - rh);
289 const Rect cand{ox(rng), oy(rng), rw, rh};
290 bool ok = true;
291 for (const Rect &other : rooms) {
292 if (rectsOverlap(cand, other, spacing)) { ok = false; break; }
293 }
294 if (ok) {
295 roomParents.push_back(rooms.empty() ? 0 : rooms.size() - 1);
296 rooms.push_back(cand);
297 break;
298 }
299 }
300 }
301 }
302 }
303 if (rooms.empty()) {
304 // Last resort: one central room.
305 const int rw = std::max(roomMin, w / 3);
306 const int rh = std::max(roomMin, h / 3);
307 rooms.push_back({(w - rw) / 2, (h - rh) / 2, rw, rh});
308 roomParents.push_back(0);
309 }
310
311 // 2) Carve room floors.
312 for (const Rect &r : rooms) carveRect(out, r, Semantic::Floor);
313
314 // 3) Connect rooms. Nearest-parent mode produces a compact branching tree
315 // instead of long row-wrap corridors while preserving deterministic output.
316 for (size_t i = 1; i < rooms.size(); ++i) {
317 size_t parent = i - 1;
318 if (connections == "growth" && i < roomParents.size()) {
319 parent = roomParents[i];
320 } else if (connections == "nearest") {
321 int bestDistance = w + h + 1;
322 const int bx = rooms[i].x + rooms[i].w / 2;
323 const int by = rooms[i].y + rooms[i].h / 2;
324 for (size_t candidate = 0; candidate < i; ++candidate) {
325 const int ax = rooms[candidate].x + rooms[candidate].w / 2;
326 const int ay = rooms[candidate].y + rooms[candidate].h / 2;
327 const int distance = std::abs(ax - bx) + std::abs(ay - by);
328 if (distance < bestDistance) {
329 bestDistance = distance;
330 parent = candidate;
331 }
332 }
333 }
334 const int ax = rooms[parent].x + rooms[parent].w / 2;
335 const int ay = rooms[parent].y + rooms[parent].h / 2;
336 const int bx = rooms[i].x + rooms[i].w / 2;
337 const int by = rooms[i].y + rooms[i].h / 2;
338 carveCorridor(out, ax, ay, bx, by, corridorW, style, rng);
339 }
340
341 // 4) Wall autotile direction masks.
342 if (doAutotile) autotileWalls(out);
343
344 // 5) Floor pattern variants.
345 const uint32_t salt = seed * 2654435761u;
346 for (int y = 0; y < h; ++y) {
347 for (int x = 0; x < w; ++x) {
348 const uint32_t c = uint32_t(out.getCell(x, y));
349 if (c == Semantic::Floor || c == Semantic::Corridor) {
350 out.setDetail(x, y, floorVariant(x, y, pattern, variants, salt));
351 }
352 }
353 }
354
355 // 6) Scatter non-blocking ground detail, avoiding narrow doorways.
356 // through doorways (cells with a wall on two opposite sides) and the
357 // outer border so spawn/stairs stay reachable.
358 std::vector<std::pair<int, int>> floorCells;
359 floorCells.reserve(size_t(w * h));
360 for (int y = 1; y < h - 1; ++y)
361 for (int x = 1; x < w - 1; ++x)
362 if (isWalkable(uint32_t(out.getCell(x, y)))) floorCells.emplace_back(x, y);
363
364 std::vector<std::pair<int, int>> decorTiles;
365 if (!floorCells.empty() && decorDensity > 0.f) {
366 const size_t count = size_t(float(floorCells.size()) * decorDensity);
367 std::uniform_int_distribution<size_t> pick(0, floorCells.size() - 1);
368 std::uniform_int_distribution<int> kind(0, 2);
369 for (size_t i = 0; i < count; ++i) {
370 const auto [dx, dy] = floorCells[pick(rng)];
371 if (out.getDetail(dx, dy) >= kDecorBase) continue;
372 // Keep a clear 1-tile ring around spawn/stairs for reachability.
373 bool nearMarker = false;
374 for (int k = 0; k < out.getObjectCount(); ++k) {
375 if (int(out.getObjectX(k)) == dx && int(out.getObjectY(k)) == dy) {
376 nearMarker = true;
377 break;
378 }
379 }
380 if (nearMarker) continue;
381 out.setDetail(dx, dy, kDecorBase + kind(rng));
382 decorTiles.emplace_back(dx, dy);
383 }
384 }
385
386 // 7) Semantic asset dressing. Pools are comma-separated asset ids supplied
387 // by the caller. Generic fallbacks deliberately contain no KayKit names.
388 // Renderers resolve `getObjectAsset()` however they like (GLTF, prefab, sprite…).
389 const auto containers = assetPool(params, "container", "container");
390 const auto treasure = assetPool(params, "treasure", "treasure");
391 const auto columns = assetPool(params, "column", "column");
392 const auto tables = assetPool(params, "table", "table");
393 const auto seating = assetPool(params, "seating", "seat");
394 const auto beds = assetPool(params, "bed", "bed");
395 const auto shelves = assetPool(params, "shelf", "shelf");
396 const auto lights = assetPool(params, "light", "light");
397 const auto wallLights = assetPool(params, "wallLight",
398 params.getString("assets.light", "light"));
399 const auto wallShelves = assetPool(params, "wallShelf",
400 params.getString("assets.shelf", "shelf"));
401 const auto banners = assetPool(params, "banner", "banner");
402 const auto weapons = assetPool(params, "weapon", "weapon_display");
403 const auto traps = assetPool(params, "trap", "trap");
404 const auto food = assetPool(params, "food", "food");
405 const auto tavern = assetPool(params, "tavern", "tavern_prop");
406 const auto clutter = assetPool(params, "clutter", "clutter");
407 const auto stairsAssets = assetPool(params, "stairs", "stairs");
408 const float propDensity = std::clamp(params.getFloat("propDensity", 0.16f), 0.f, 1.f);
409 const float corridorLightDensity =
410 std::clamp(params.getFloat("corridorLightDensity", 0.035f), 0.f, 0.25f);
411 std::unordered_set<int> occupied;
412 int objectSerial = 0;
413 int minimumRoomProps = std::numeric_limits<int>::max();
414 auto cellKey = [w](int x, int y) { return y * w + x; };
415 auto place = [&](const std::string &role, const std::vector<std::string> &pool, double x, double y,
416 float rotation, int flags, float ow = 1.f, float oh = 1.f) {
417 const int cellX = clampInt(int(std::round(x)), 0, w - 1);
418 const int cellY = clampInt(int(std::round(y)), 0, h - 1);
419 if (pool.empty() || !isWalkable(uint32_t(out.getCell(cellX, cellY)))) return false;
420 if ((flags & 1) && occupied.count(cellKey(cellX, cellY))) return false;
421 out.addAssetObject(role + std::to_string(objectSerial++), role, pickAsset(pool, rng),
422 static_cast<float>(x), static_cast<float>(y), ow, oh, rotation, flags);
423 if (flags & 1) occupied.insert(cellKey(cellX, cellY));
424 return true;
425 };
426
427 if (decorSet != "none") {
428 for (size_t roomIndex = 0; roomIndex < rooms.size(); ++roomIndex) {
429 const Rect &r = rooms[roomIndex];
430 if (r.w < 3 || r.h < 3) continue;
431 const int left = r.x, right = r.x + r.w - 1;
432 const int top = r.y, bottom = r.y + r.h - 1;
433 const int cx = r.x + r.w / 2, cy = r.y + r.h / 2;
434 // The clustered layout grows from room zero, making it the visual
435 // hub. Give that hub a dense dining composition instead of a quiet
436 // edge-oriented theme; remaining rooms still cycle deterministically.
437 const int theme = roomIndex == 0 ? 2 : int((roomIndex + seed) % 7);
438 static constexpr const char *kThemeNames[] = {
439 "storage", "quarters", "dining", "armory", "treasury", "shrine", "tavern"};
440 out.addAssetObject("room" + std::to_string(roomIndex), "room",
441 kThemeNames[theme], float(r.x), float(r.y),
442 float(r.w), float(r.h), 0.f, 32);
443 const int propsBeforeRoom = objectSerial;
444
445 // Every furnished room receives wall-mounted light and occasional banner.
446 place("light", wallLights, left, cy, 90.f, 2 | 4);
447 if (r.w >= 6) place("light", wallLights, right, cy, 270.f, 2 | 4);
448 if ((rng() % 3u) != 0) place("banner", banners, left, cy - 1, 90.f, 2);
449 if (propDensity >= 0.45f && r.w >= 5 && (roomIndex % 2) == 0)
450 place("banner", banners, cx + (r.w >= 7 ? 1 : 0), bottom, 0.f, 2);
451 if (propDensity >= 0.6f && r.w >= 6 && (roomIndex % 2) != 0)
452 place("light", wallLights, cx, bottom, 0.f, 2 | 4);
453
454 if (decorSet == "pillars") {
455 place("column", columns, left + 1, top + 1, 0.f, 1);
456 place("column", columns, right - 1, bottom - 1, 0.f, 1);
457 continue;
458 }
459 if (decorSet == "treasure" || theme == 4) {
460 place("container", containers, right, cy, 270.f, 1);
461 place("treasure", treasure, right - 1, cy, 0.f, 16);
462 place("treasure", treasure, right - 1.35f, cy + 0.45f, 35.f, 16);
463 place("treasure", treasure, right - 0.65f, cy - 0.45f, 320.f, 16);
464 place("column", columns, left + 1, top + 1, 0.f, 1);
465 place("column", columns, left + 1, bottom - 1, 0.f, 1);
466 } else if (theme == 0) {
467 place("container", containers, right, top + 1, 270.f, 1);
468 place("container", containers, right, bottom - 1, 270.f, 1);
469 place("container", containers, right - 0.75f, top + 1.35f, 245.f, 16);
470 place("container", containers, right - 0.55f, bottom - 1.25f, 300.f, 16);
471 place("shelf", wallShelves, cx, bottom, 0.f, 1 | 2);
472 if (r.h >= 6) place("shelf", wallShelves, left, cy + 1, 90.f, 1 | 2);
473 if (r.w >= 6) place("container", containers, left + 2, bottom, 0.f, 1);
474 place("clutter", clutter, left + 1, bottom, 0.f, 16);
475 } else if (theme == 1) {
476 place("bed", beds, right, cy, 270.f, 1, 1.f, 2.f);
477 if (r.h >= 6) place("bed", beds, right, cy - 2, 270.f, 1, 1.f, 2.f);
478 place("container", containers, right - 1, top, 180.f, 1);
479 place("light", lights, right - 1, cy, 0.f, 4);
480 place("seating", seating, left + 1, bottom - 1, 45.f, 1);
481 } else if (theme == 2) {
482 place("table", tables, cx, cy, (rng() & 1u) ? 0.f : 90.f, 1, 2.f, 1.f);
483 place("seating", seating, cx - 1, cy, 270.f, 1);
484 place("seating", seating, cx + 1, cy, 90.f, 1);
485 place("seating", seating, cx, cy - 1, 0.f, 1);
486 place("seating", seating, cx, cy + 1, 180.f, 1);
487 place("food", food, cx, cy, 0.f, 16);
488 place("food", food, cx - 0.38f, cy + 0.18f, 25.f, 16);
489 place("food", food, cx + 0.38f, cy - 0.18f, 205.f, 16);
490 if (r.w >= 6) {
491 place("table", tables, cx - 2, cy, 0.f, 1, 2.f, 1.f);
492 place("seating", seating, cx - 2, cy - 1, 0.f, 1);
493 place("seating", seating, cx - 2, cy + 1, 180.f, 1);
494 place("food", food, cx - 2, cy, 70.f, 16);
495 }
496 } else if (theme == 3) {
497 place("weapon", weapons, left, cy, 90.f, 2);
498 place("weapon", weapons, cx, bottom, 0.f, 2);
499 place("column", columns, cx, cy, 0.f, 1);
500 if (r.w >= 6) place("column", columns, left + 1, bottom - 1, 0.f, 1);
501 place("container", containers, right, top + 1, 270.f, 1);
502 if ((rng() & 1u) != 0) place("trap", traps, cx - 1, cy, 0.f, 8);
503 } else if (theme == 5) {
504 place("table", tables, cx, cy, 0.f, 1, 2.f, 1.f);
505 place("light", lights, cx - 0.45f, cy, 0.f, 4);
506 place("light", lights, cx + 0.45f, cy, 0.f, 4);
507 place("food", food, cx, cy, 0.f, 16);
508 place("seating", seating, cx, bottom - 1, 180.f, 1);
509 if (r.w >= 6) {
510 place("seating", seating, cx - 2, bottom - 1, 180.f, 1);
511 place("seating", seating, cx + 2, bottom - 1, 180.f, 1);
512 }
513 } else {
514 place("tavern", tavern, right, cy, 270.f, 1);
515 place("table", tables, cx, cy, 0.f, 1, 2.f, 1.f);
516 place("seating", seating, cx - 1, cy, 270.f, 1);
517 place("seating", seating, cx + 1, cy, 90.f, 1);
518 place("seating", seating, cx, cy + 1, 180.f, 1);
519 place("food", food, cx, cy, 0.f, 16);
520 place("food", food, cx - 0.4f, cy, 20.f, 16);
521 place("food", food, cx + 0.4f, cy, 340.f, 16);
522 place("container", containers, right, bottom - 1, 270.f, 1);
523 place("tavern", tavern, right - 0.7f, bottom - 1.2f, 250.f, 16);
524 }
525
526 // High-density presets add a secondary wall-side storage vignette.
527 // This is intentionally shared across themes: barrels, trunks, and
528 // crates are the visual glue that makes modular rooms feel occupied.
529 if (propDensity >= 0.45f && theme != 0 && theme != 4 && theme != 6) {
530 place("container", containers, right, bottom - 1, 270.f, 1);
531 place("container", containers, right - 0.65f, bottom - 1.3f, 245.f, 16);
532 if (r.w >= 6 && theme != 2)
533 place("seating", seating, left + 1, top + 1, 45.f, 1);
534 }
535
536 // Sparse edge clutter gives the dense, lived-in reference look without
537 // turning room centres and corridors into an obstacle field.
538 const int clutterAttempts = clampInt(int(std::ceil(propDensity * 1.5f)), 0, 2);
539 const std::pair<int, int> clutterSpots[] = {
540 {left, top + 1}, {right, bottom - 1}, {left + 1, bottom}, {right - 1, top}};
541 for (int i = 0; i < clutterAttempts; ++i) {
542 const auto &spot = clutterSpots[(size_t(i) + roomIndex) % 4];
543 place("clutter", clutter, spot.first, spot.second,
544 float((i + int(roomIndex)) % 4) * 90.f, 16);
545 }
546
547 // Enforce a visible minimum for dense showcase presets. Failed
548 // blocking placements simply advance to another interior edge;
549 // non-blocking clutter remains cosmetic and navigation-safe.
550 if (propDensity >= 0.35f) {
551 // Scale the visual budget with room area. A fixed count makes
552 // large chambers look abandoned even at high density.
553 const int roomArea = r.w * r.h;
554 const int targetProps = clampInt(
555 4 + int(std::ceil(propDensity * float(roomArea) * 0.28f)), 4, 16);
556 const std::pair<int, int> fillerSpots[] = {
557 {left + 1, top + 1}, {right - 1, top + 1},
558 {left + 1, bottom - 1}, {right - 1, bottom - 1},
559 {cx - 1, bottom - 1}, {cx + 1, top + 1}};
560 for (int attempt = 0;
561 attempt < 24 && objectSerial - propsBeforeRoom < targetProps; ++attempt) {
562 const auto &spot = fillerSpots[(size_t(attempt) + roomIndex) % 6];
563 if ((attempt & 1) == 0)
564 place("container", containers, spot.first, spot.second,
565 float((attempt + int(roomIndex)) % 4) * 90.f, 1);
566 else
567 place("container", containers, float(spot.first) + 0.35f,
568 float(spot.second) - 0.25f,
569 float((attempt * 37) % 360), 16);
570 }
571 }
572 minimumRoomProps = std::min(minimumRoomProps, objectSerial - propsBeforeRoom);
573 }
574
575 // Long connectors in the reference are punctuated by sparse sconces.
576 // Keep this semantic and density-driven so packs can substitute any
577 // wall-mounted light prefab without changing the generator.
578 const uint32_t lightThreshold = uint32_t(corridorLightDensity * 1000.f);
579 for (int y = 1; y < h - 1; ++y) {
580 for (int x = 1; x < w - 1; ++x) {
581 if (uint32_t(out.getCell(x, y)) != Semantic::Corridor) continue;
582 const uint32_t hash = uint32_t(x) * 73856093u ^ uint32_t(y) * 19349663u ^ seed;
583 if ((hash % 1000u) >= lightThreshold) continue;
584 if (uint32_t(out.getCell(x - 1, y)) == Semantic::Wall)
585 place("light", wallLights, float(x), float(y), 90.f, 2 | 4);
586 else if (uint32_t(out.getCell(x + 1, y)) == Semantic::Wall)
587 place("light", wallLights, float(x), float(y), 270.f, 2 | 4);
588 else if (uint32_t(out.getCell(x, y - 1)) == Semantic::Wall)
589 place("light", wallLights, float(x), float(y), 180.f, 2 | 4);
590 else if (uint32_t(out.getCell(x, y + 1)) == Semantic::Wall)
591 place("light", wallLights, float(x), float(y), 0.f, 2 | 4);
592 }
593 }
594 }
595
596 // 8) Spawn + an outward-facing perimeter stair. Stairs are architecture,
597 // not arbitrary floor clutter: orientation identifies the wall opening the
598 // renderer should replace (0 north, 180 south, 90 west, 270 east).
599 struct StairCandidate { int x, y; float rotation; int side; };
600 std::vector<StairCandidate> stairCandidates;
601 for (const Rect &r : rooms) {
602 for (int x = r.x + 1; x < r.x + r.w - 1; ++x) {
603 if ((stairSideMask & 1) && uint32_t(out.getCell(x, r.y - 1)) == Semantic::Wall)
604 stairCandidates.push_back({x, r.y, 180.f, 1});
605 if ((stairSideMask & 2) && uint32_t(out.getCell(x, r.y + r.h)) == Semantic::Wall)
606 stairCandidates.push_back({x, r.y + r.h - 1, 0.f, 2});
607 }
608 for (int y = r.y + 1; y < r.y + r.h - 1; ++y) {
609 if ((stairSideMask & 4) && uint32_t(out.getCell(r.x - 1, y)) == Semantic::Wall)
610 stairCandidates.push_back({r.x, y, 270.f, 4});
611 if ((stairSideMask & 8) && uint32_t(out.getCell(r.x + r.w, y)) == Semantic::Wall)
612 stairCandidates.push_back({r.x + r.w - 1, y, 90.f, 8});
613 }
614 }
615 std::vector<size_t> selectedStairs;
616 for (int entrance = 0;
617 entrance < stairCount && selectedStairs.size() < stairCandidates.size(); ++entrance) {
618 size_t best = size_t(seed) % stairCandidates.size();
619 int bestDistance = -1;
620 if (!selectedStairs.empty()) {
621 for (size_t candidate = 0; candidate < stairCandidates.size(); ++candidate) {
622 bool duplicate = false;
623 int nearest = w + h;
624 for (size_t chosen : selectedStairs) {
625 if (stairCandidates[candidate].x == stairCandidates[chosen].x &&
626 stairCandidates[candidate].y == stairCandidates[chosen].y) {
627 duplicate = true;
628 break;
629 }
630 nearest = std::min(nearest,
631 std::abs(stairCandidates[candidate].x - stairCandidates[chosen].x) +
632 std::abs(stairCandidates[candidate].y - stairCandidates[chosen].y));
633 }
634 if (!duplicate && nearest > bestDistance) {
635 bestDistance = nearest;
636 best = candidate;
637 }
638 }
639 if (bestDistance < 0) break;
640 }
641 selectedStairs.push_back(best);
642 const StairCandidate &stairs = stairCandidates[best];
643 out.addAssetObject("stairs" + std::to_string(entrance), "stairs",
644 pickAsset(stairsAssets, rng), float(stairs.x), float(stairs.y),
645 1.f, 1.f, stairs.rotation, 64);
646 occupied.insert(cellKey(stairs.x, stairs.y));
647 }
648
649 std::vector<std::pair<int, int>> walkable;
650 for (int y = 0; y < h; ++y)
651 for (int x = 0; x < w; ++x)
652 if (isWalkable(uint32_t(out.getCell(x, y))) && !occupied.count(cellKey(x, y)))
653 walkable.emplace_back(x, y);
654 if (!walkable.empty()) {
655 auto pick = [&](uint32_t s) {
656 return walkable[(seed * 1664525u + s * 1013904223u) % walkable.size()];
657 };
658 auto a = pick(1);
659 out.addObjectAt("spawn", "spawn", float(a.first), float(a.second));
660 }
661
662 // 9) Metadata.
663 out.setMeta("algorithm", "level.roguelike");
664 out.setMeta("seed", std::to_string(seed));
665 out.setMeta("rooms", std::to_string(rooms.size()));
666 out.setMeta("floorPattern", pattern);
667 out.setMeta("decorTiles", std::to_string(decorTiles.size()));
668 out.setMeta("corridorStyle", style);
669 out.setMeta("layoutStyle", layout);
670 out.setMeta("connectionStyle", connections);
671 out.setMeta("assetPack", params.getString("assetPack", "semantic-default"));
672 out.setMeta("placedProps", std::to_string(objectSerial));
673 out.setMeta("minimumRoomProps",
674 std::to_string(minimumRoomProps == std::numeric_limits<int>::max()
675 ? 0 : minimumRoomProps));
676 out.setMeta("stairs", std::to_string(selectedStairs.size()));
677 // Tile renderers use these semantic pools to resolve every architecture cell.
678 // Copying them into metadata keeps the generated artifact self-describing.
679 static const char *architectureRoles[] = {
680 "wall", "wallCorner", "wallJunction", "wallDoor", "wallWindow", "wallHalf",
681 "wallBroken", "wallScaffold", "floor", "floorBroken", "floorDirt", "floorWood",
682 "floorGrate", "floorFoundation", "ceiling", "stairs", "stairsRail", "door", "barrier"};
683 for (const char *role : architectureRoles) {
684 const std::string key = std::string("assets.") + role;
685 if (params.has(key)) out.setMeta(key, params.getString(key, ""));
686 }
687 return true;
688}
689
690} // namespace
691
693 auto descriptor = GeneratorDescriptor::grid("level.roguelike", "Roguelike Level", "Dungeon", 9, 9);
694 descriptor.params.push_back(ParamDescriptor::integer("roomCount", "Room Count", 9, 1, 256));
695 descriptor.params.push_back(ParamDescriptor::integer("roomMin", "Minimum Room Size", 4, 2, 128));
696 descriptor.params.push_back(ParamDescriptor::integer("roomMax", "Maximum Room Size", 8, 2, 256));
697 descriptor.params.push_back(ParamDescriptor::integer("padding", "Room Padding", 1, 0, 4));
698 descriptor.params.push_back(ParamDescriptor::integer("spacing", "Room Spacing", 2, 0, 8));
699 descriptor.params.push_back(ParamDescriptor::integer(
700 "clusterGapMin", "Cluster Minimum Gap", 2, 1, 8));
701 descriptor.params.push_back(ParamDescriptor::integer(
702 "clusterGapMax", "Cluster Maximum Gap", 4, 1, 12));
703 descriptor.params.push_back(ParamDescriptor::integer(
704 "clusterBranchBias", "Cluster Branch Bias", 0, 0, 4));
705 descriptor.params.push_back(ParamDescriptor::integer("corridorWidth", "Corridor Width", 1, 1, 3));
706 descriptor.params.push_back(ParamDescriptor::integer("stairCount", "Stair Count", 1, 0, 4));
707 descriptor.params.push_back(ParamDescriptor::integer(
708 "stairSideMask", "Stair Side Mask", 15, 1, 15));
709 descriptor.params.push_back(ParamDescriptor::choice("layoutStyle", "Room Layout", "grid",
710 {"grid", "clustered"}));
711 descriptor.params.push_back(ParamDescriptor::choice("connectionStyle", "Room Connections",
712 "sequential",
713 {"sequential", "nearest", "growth"}));
714 descriptor.params.push_back(ParamDescriptor::choice("corridorStyle", "Corridor Style", "l",
715 {"l", "straight", "diagonal"}));
716 descriptor.params.push_back(ParamDescriptor::choice("floorPattern", "Floor Pattern", "brick",
717 {"brick", "checker", "plank", "plain", "cobble"}));
718 descriptor.params.push_back(ParamDescriptor::integer("floorVariants", "Floor Variants", 4, 1, 15));
719 descriptor.params.push_back(ParamDescriptor::floating("decorDensity", "Decoration Density", 0.05f, 0.f,
720 1.f, 0.01f));
721 descriptor.params.push_back(ParamDescriptor::choice("decorSet", "Decoration Set", "mixed",
722 {"mixed", "pillars", "treasure", "none"}));
723 descriptor.params.push_back(ParamDescriptor::floating("propDensity", "Prop Density", 0.16f,
724 0.f, 1.f, 0.01f));
725 descriptor.params.push_back(ParamDescriptor::floating("corridorLightDensity",
726 "Corridor Light Density", 0.035f,
727 0.f, 0.25f, 0.005f));
728 descriptor.params.push_back(ParamDescriptor::boolean("autotile", "Autotile", true));
729 registry.registerAlgorithm(std::move(descriptor), genRoguelike);
730}
731
733 autotileWalls(grid);
734 return grid.getWidth() > 0 && grid.getHeight() > 0;
735}
736
738 if (grid.getWidth() <= 0 || grid.getHeight() <= 0)
740 eve::DiagnosticCode::InvalidArgument, "autotile grid dimensions must be positive", {}, {},
741 "procgen.gridGraph"));
742 constexpr int dx[] = {0, 1, 0, -1, 1, 1, -1, -1};
743 constexpr int dy[] = {-1, 0, 1, 0, -1, 1, 1, -1};
744 for (int y = 0; y < grid.getHeight(); ++y) {
745 for (int x = 0; x < grid.getWidth(); ++x) {
746 int mask = 0;
747 if (uint32_t(grid.getCell(x, y)) != Semantic::Empty)
748 for (int direction = 0; direction < 8; ++direction)
749 if (uint32_t(grid.getCell(x + dx[direction], y + dy[direction])) != Semantic::Empty)
750 mask |= 1 << direction;
751 grid.setDetail(x, y, mask);
752 }
753 }
755}
756
757uint32_t randomSeedValue() {
758 std::random_device rd;
759 const uint32_t v = rd();
760 return v == 0 ? 1u : v;
761}
762
763} // namespace eve::procgen
SQInteger top
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
std::string descriptor
const std::string & s
int mask
float gap
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
int ax
Definition CaveMesh.cpp:113
int ay
Definition CaveMesh.cpp:113
int bx
Definition CaveMesh.cpp:114
int by
Definition CaveMesh.cpp:114
Object::Ptr theme
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
EvpackChunkInput input
Definition Evpack.cpp:170
int rows
int cols
std::string pattern
std::string layout
std::vector< eve::ProcgenProbeDesc > lights
std::uint32_t key
double r
float v
HexVec3 left
HexVec3 right
std::int32_t c
std::int32_t first
int h
std::uint32_t width
TokenKind kind
std::array< float, 4 > rotation
std::int32_t parent
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
std::string error
Definition Package.cpp:60
std::uint32_t seed
Definition PointSet.cpp:807
RoadLaneDirection direction
float dy
float dx
bool occupied
std::uint32_t count
std::map< Cell, int > best
ecs::EntityHandle side
int spacing
int columns
float size
Definition TreeMesh.cpp:156
double oy
double ox
uint32_t semantic
Definition WfcSimple.cpp:15
float bottom
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Registry for executable generators and their reflection metadata.
void registerAlgorithm(const std::string &id, GeneratorFn fn)
Register an algorithm without metadata for backward compatibility.
Intermediate 2D generation result. cells store semantic ids (see Semantic.h), not tile GIDs — convert...
Definition Grid2D.h:36
std::vector< ParamSpec > params
constexpr uint32_t Floor
Definition Semantic.h:14
constexpr uint32_t Wall
Definition Semantic.h:13
constexpr uint32_t Corridor
Definition Semantic.h:15
double sample(const Heightmap &map, double u, double v)
Sample.
void registerRoguelikeGenerator(GeneratorRegistry &registry)
Register the "level.roguelike" algorithm (idempotent).
eve::Result< void > autotileOccupiedGridInPlace(Grid2D &grid)
Write an eight-neighbour occupancy mask into every non-empty cell's detail value.
bool autotileGridInPlace(Grid2D &grid)
Post-process any generated Grid2D: fill each wall cell's detail with an 8-bit neighbour mask describi...
uint32_t randomSeedValue()
Produce a fresh seed suitable for regenerating a level (never 0).
uint32_t pad[2]