feat(procgen): connect dungeon growth branches directly

This commit is contained in:
sunxfancy committed 2026-08-28 20:08:55 +08:00
1 parent b6ecb576a0
commit 1aa670ea25
6 files changed
+35 -16

No files matched your search

+3 -2
View File
@@ -5,8 +5,9 @@ It builds floors and boundary walls from semantic cells, then instantiates the
generator's automatically placed themed props with their emitted asset id, yaw,
footprint and placement flags.
The showcase uses the generator's `clustered` room growth and nearest-parent
connections. `clusterGapMin` and `clusterGapMax` configure the structural gap
The showcase uses the generator's `clustered` room growth and `growth`
connections, which preserve the generated parent-child tree and make short,
direct door links. `clusterGapMin` and `clusterGapMax` configure the structural gap
between clustered rooms; setting both to `1` produces the compact, short-link
composition used here. It also uses an orthographic camera framed from the occupied bounds, merged
cut-corner stone floor meshes, modular wall/corner/doorway pieces, perimeter
Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 419 KiB

+8 -7
View File
@@ -235,7 +235,7 @@ function rebuildDungeon() {
p.setInt("corridorWidth", 1);
p.setString("layoutStyle", "clustered");
p.setString("corridorStyle", "l");
p.setString("connectionStyle", "nearest");
p.setString("connectionStyle", "growth");
p.setString("floorPattern", "cobble");
p.setString("decorSet", "mixed");
p.setFloat("decorDensity", 0.055);
@@ -407,12 +407,13 @@ function rebuildDungeon() {
// authored at floor origin, so overlays sharing a table cell are raised.
if (role == "light" && (flags & 2) != 0) py = 2.15;
else if (role == "weapon") py = 2.0;
else if (role == "banner") py = 1.35;
else if (role == "food") py = 1.88;
local propScale = 1.0;
if (role == "table" || role == "tavern") propScale = 1.65;
else if (role == "bed") propScale = 1.45;
else if (role == "banner") propScale = 1.18;
else if (role == "banner") propScale = 1.42;
else if (role == "seating" || role == "container") propScale = 1.55;
else if (role == "treasure") propScale = 1.40;
addAsset(id, px, py, pz, yaw, propScale, propScale, propScale);
@@ -424,8 +425,8 @@ function rebuildDungeon() {
local light = eve.Light3D();
light.setType("point");
light.setPosition(candidate[0], candidate[1], candidate[2]);
light.setColor(1.0, 0.55, 0.25, 3.2);
light.setRadius(DUNGEON_CELL * 3.0);
light.setColor(1.0, 0.62, 0.34, 2.45);
light.setRadius(DUNGEON_CELL * 3.6);
dungeonLights.append(light);
}
}
@@ -438,12 +439,12 @@ if (dungeonCamera == null) {
dungeonCamera.setFov(58.0);
dungeonCamera.setOrthographic(190.0);
dungeonCamera.setClipPlanes(0.1, 400.0);
dungeonCamera.setAmbient(0.62, 0.64, 0.70);
dungeonCamera.setAmbient(0.76, 0.78, 0.84);
dungeonCamera.setActive(true);
gfx.setDirectionalLight(-0.45, -1.0, -0.25, 1.75, 1.62, 1.45);
gfx.setDirectionalLight(-0.45, -1.0, -0.25, 2.05, 1.92, 1.76);
}
if (dungeonGrid == null) rebuildDungeon();
gfx.setBackgroundColor(0.025, 0.015, 0.055, 1.0);
gfx.setBackgroundColor(0.045, 0.018, 0.085, 1.0);
function eve_update(dt) {
if (keyboard.isDown("r") || keyboard.isDown("R")) {
@@ -38,7 +38,10 @@ function configureDungeonAssetPack(p) {
p.setString("assets.light", "candle,candle_lit,candle_melted,candle_thin,candle_thin_lit,candle_triple,torch,torch_lit,torch_mounted");
p.setString("assets.wallShelf", "shelf_small,shelf_small_candles,wall_shelves");
p.setString("assets.wallLight", "torch_mounted");
p.setString("assets.banner", "banner_blue,banner_brown,banner_green,banner_red,banner_white,banner_yellow,banner_patternA_blue,banner_patternA_brown,banner_patternA_green,banner_patternA_red,banner_patternA_white,banner_patternA_yellow,banner_patternB_blue,banner_patternB_brown,banner_patternB_green,banner_patternB_red,banner_patternB_white,banner_patternB_yellow,banner_patternC_blue,banner_patternC_brown,banner_patternC_green,banner_patternC_red,banner_patternC_white,banner_patternC_yellow,banner_shield_blue,banner_shield_brown,banner_shield_green,banner_shield_red,banner_shield_white,banner_shield_yellow,banner_thin_blue,banner_thin_brown,banner_thin_green,banner_thin_red,banner_thin_white,banner_thin_yellow,banner_triple_blue,banner_triple_brown,banner_triple_green,banner_triple_red,banner_triple_white,banner_triple_yellow");
// Repeated ids are weights, not hard-coded renderer behavior. The showcase
// favours blue cloth like the reference while retaining every colour and
// silhouette in the configurable pool.
p.setString("assets.banner", "banner_blue,banner_blue,banner_blue,banner_patternA_blue,banner_patternA_blue,banner_patternB_blue,banner_patternC_blue,banner_shield_blue,banner_thin_blue,banner_triple_blue,banner_triple_blue,banner_brown,banner_green,banner_red,banner_white,banner_yellow,banner_patternA_brown,banner_patternA_green,banner_patternA_red,banner_patternA_white,banner_patternA_yellow,banner_patternB_brown,banner_patternB_green,banner_patternB_red,banner_patternB_white,banner_patternB_yellow,banner_patternC_brown,banner_patternC_green,banner_patternC_red,banner_patternC_white,banner_patternC_yellow,banner_shield_brown,banner_shield_green,banner_shield_red,banner_shield_white,banner_shield_yellow,banner_thin_brown,banner_thin_green,banner_thin_red,banner_thin_white,banner_thin_yellow,banner_triple_brown,banner_triple_green,banner_triple_red,banner_triple_white,banner_triple_yellow");
p.setString("assets.weapon", "sword_shield,sword_shield_broken,sword_shield_gold");
p.setString("assets.trap", "spikes,floor_tile_big_spikes");
p.setString("assets.food", "plate,plate_small,plate_stack,plate_food_A,plate_food_B,bottle_A_brown,bottle_A_green,bottle_A_labeled_brown,bottle_A_labeled_green,bottle_B_brown,bottle_B_green,bottle_C_brown,bottle_C_green");
@@ -214,6 +214,7 @@ bool genRoguelike(const Params &params, Grid2D &out, std::string &error) {
const int cols = std::max(1, int(std::sqrt(double(roomCount))));
const int rows = std::max(1, (roomCount + cols - 1) / cols);
std::vector<Rect> rooms;
std::vector<size_t> roomParents;
std::uniform_int_distribution<int> dim(roomMin, roomMax);
std::uniform_int_distribution<int> jx(0, 0), jy(0, 0);
@@ -221,11 +222,13 @@ bool genRoguelike(const Params &params, Grid2D &out, std::string &error) {
const int firstW = std::min(dim(rng), w - padding * 2);
const int firstH = std::min(dim(rng), h - padding * 2);
rooms.push_back({(w - firstW) / 2, (h - firstH) / 2, firstW, firstH});
roomParents.push_back(0);
std::uniform_int_distribution<int> jitter(-1, 1);
std::uniform_int_distribution<int> clusterGap(clusterGapMin, clusterGapMax);
const int maxAttempts = roomCount * 160;
for (int attempt = 0; attempt < maxAttempts && int(rooms.size()) < roomCount; ++attempt) {
const Rect &parent = rooms[size_t(rng()) % rooms.size()];
const size_t parentIndex = size_t(rng()) % rooms.size();
const Rect &parent = rooms[parentIndex];
const int rw = std::min(dim(rng), w - padding * 2);
const int rh = std::min(dim(rng), h - padding * 2);
const int gap = clusterGap(rng);
@@ -254,7 +257,10 @@ bool genRoguelike(const Params &params, Grid2D &out, std::string &error) {
for (const Rect &other : rooms) {
if (rectsOverlap(cand, other, spacing)) { ok = false; break; }
}
if (ok) rooms.push_back(cand);
if (ok) {
rooms.push_back(cand);
roomParents.push_back(parentIndex);
}
}
} else {
for (int r = 0; r < rows; ++r) {
@@ -277,7 +283,11 @@ bool genRoguelike(const Params &params, Grid2D &out, std::string &error) {
for (const Rect &other : rooms) {
if (rectsOverlap(cand, other, spacing)) { ok = false; break; }
}
if (ok) { rooms.push_back(cand); break; }
if (ok) {
roomParents.push_back(rooms.empty() ? 0 : rooms.size() - 1);
rooms.push_back(cand);
break;
}
}
}
}
@@ -287,6 +297,7 @@ bool genRoguelike(const Params &params, Grid2D &out, std::string &error) {
const int rw = std::max(roomMin, w / 3);
const int rh = std::max(roomMin, h / 3);
rooms.push_back({(w - rw) / 2, (h - rh) / 2, rw, rh});
roomParents.push_back(0);
}
// 2) Carve room floors.
@@ -296,7 +307,9 @@ bool genRoguelike(const Params &params, Grid2D &out, std::string &error) {
// instead of long row-wrap corridors while preserving deterministic output.
for (size_t i = 1; i < rooms.size(); ++i) {
size_t parent = i - 1;
if (connections == "nearest") {
if (connections == "growth" && i < roomParents.size()) {
parent = roomParents[i];
} else if (connections == "nearest") {
int bestDistance = w + h + 1;
const int bx = rooms[i].x + rooms[i].w / 2;
const int by = rooms[i].y + rooms[i].h / 2;
@@ -652,7 +665,8 @@ void registerRoguelikeGenerator(GeneratorRegistry &registry) {
descriptor.params.push_back(ParamDescriptor::choice("layoutStyle", "Room Layout", "grid",
{"grid", "clustered"}));
descriptor.params.push_back(ParamDescriptor::choice("connectionStyle", "Room Connections",
"sequential", {"sequential", "nearest"}));
"sequential",
{"sequential", "nearest", "growth"}));
descriptor.params.push_back(ParamDescriptor::choice("corridorStyle", "Corridor Style", "l",
{"l", "straight", "diagonal"}));
descriptor.params.push_back(ParamDescriptor::choice("floorPattern", "Floor Pattern", "brick",
+1 -1
View File
@@ -287,7 +287,7 @@ TEST_CASE("procgen.roguelike.rulesChangeLayout") {
pView->setString("floorPattern", pattern);
pView->setString("corridorStyle", style);
pView->setString("layoutStyle", layout);
pView->setString("connectionStyle", layout == "clustered" ? "nearest" : "sequential");
pView->setString("connectionStyle", layout == "clustered" ? "growth" : "sequential");
auto gLease = requireGrid(*mod, "level.roguelike", p.handle);
auto g = gLease.view();
REQUIRE(g.isBound());