feat: 3D blobber dungeon generator (PR 1)
Replaces the 2D-only demo pipeline with a 3D cell-based blobber generator. Per-cell face walls, per-material mesh emission, and a GDExtension binding that returns a Dictionary with ArrayMesh surfaces the demo consumes directly. - src/blobber/: cell3d_t data model, dungeon container, pipeline that wraps the 2D generator per level and materializes into cell3d - src/mesh/: face-quad emitter with per-material groups + .obj dump - src/genesis3d_main.c: new CLI driving the blobber + mesh - godot/: BrogueGen.generate_dungeon(seed, num_levels, depth) binding with dungeon_to_dict packing cells + mesh surfaces - demo/: demo_blobber.tscn + dungeon_builder.gd, func_godot addon for the .map export path, point/entity templates, TrenchBroom docs - Retired: old arcade/FPS demo scenes and their scripts, unused meshlib Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6ee49c3375
commit
7a6ae79d01
160 changed files with 7209 additions and 2072 deletions
|
|
@ -18,9 +18,13 @@ extern "C" {
|
|||
#include "gen/stairs.h"
|
||||
#include "gen/chokepoints.h"
|
||||
#include "gen/machines.h"
|
||||
#include "blobber/pipeline.h"
|
||||
#include "blobber/dungeon.h"
|
||||
#include "mesh/face_mesh.h"
|
||||
}
|
||||
|
||||
#include "grid_to_dict.h"
|
||||
#include "dungeon_to_dict.h"
|
||||
|
||||
using namespace godot;
|
||||
|
||||
|
|
@ -29,6 +33,10 @@ BrogueGen::~BrogueGen() = default;
|
|||
|
||||
void BrogueGen::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("generate", "seed", "depth"), &BrogueGen::generate);
|
||||
ClassDB::bind_method(D_METHOD("generate_dungeon", "seed", "num_levels", "depth"),
|
||||
&BrogueGen::generate_dungeon);
|
||||
ClassDB::bind_method(D_METHOD("generate_2d_slices", "seed", "num_levels", "depth"),
|
||||
&BrogueGen::generate_2d_slices);
|
||||
}
|
||||
|
||||
Dictionary BrogueGen::generate(int seed, int depth) {
|
||||
|
|
@ -66,3 +74,30 @@ Dictionary BrogueGen::generate(int seed, int depth) {
|
|||
d["gate_sites"] = gates;
|
||||
return d;
|
||||
}
|
||||
|
||||
Dictionary BrogueGen::generate_dungeon(int seed, int num_levels, int depth) {
|
||||
if (num_levels < 1) num_levels = 1;
|
||||
dungeon_t *dng = blobber_generate((uint64_t)seed, num_levels, depth);
|
||||
if (!dng) return Dictionary();
|
||||
|
||||
mesh_build_t *mb = mesh_build_from_dungeon(dng, /*cell_size=*/3.0f);
|
||||
if (!mb) {
|
||||
dungeon_destroy(dng);
|
||||
return Dictionary();
|
||||
}
|
||||
|
||||
Dictionary d = dungeon_to_dictionary(dng, mb, seed, depth, num_levels);
|
||||
|
||||
mesh_build_destroy(mb);
|
||||
dungeon_destroy(dng);
|
||||
return d;
|
||||
}
|
||||
|
||||
TypedArray<Dictionary> BrogueGen::generate_2d_slices(int seed, int num_levels, int depth) {
|
||||
if (num_levels < 1) num_levels = 1;
|
||||
dungeon_t *dng = blobber_generate((uint64_t)seed, num_levels, depth);
|
||||
if (!dng) return TypedArray<Dictionary>();
|
||||
TypedArray<Dictionary> slices = dungeon_to_2d_slices(dng);
|
||||
dungeon_destroy(dng);
|
||||
return slices;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <godot_cpp/classes/node.hpp>
|
||||
#include <godot_cpp/variant/dictionary.hpp>
|
||||
#include <godot_cpp/variant/typed_array.hpp>
|
||||
|
||||
namespace godot {
|
||||
|
||||
|
|
@ -20,6 +21,16 @@ public:
|
|||
~BrogueGen();
|
||||
|
||||
Dictionary generate(int seed, int depth);
|
||||
|
||||
/* v2 API: 3D blobber dungeon. Returns a Dictionary with raw cell data +
|
||||
per-material mesh surfaces. See dungeon_to_dict.h for the schema. */
|
||||
Dictionary generate_dungeon(int seed, int num_levels, int depth);
|
||||
|
||||
/* Exporter bridge: returns an Array of per-level Dictionaries shaped like
|
||||
the 2D generate() output ({width, height, terrain, liquid}), derived
|
||||
from a blobber 3D dungeon. Lets the .map exporter consume either
|
||||
generator uniformly. */
|
||||
TypedArray<Dictionary> generate_2d_slices(int seed, int num_levels, int depth);
|
||||
};
|
||||
|
||||
} // namespace godot
|
||||
|
|
|
|||
167
godot/src/dungeon_to_dict.cpp
Normal file
167
godot/src/dungeon_to_dict.cpp
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
#include "dungeon_to_dict.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <godot_cpp/classes/mesh.hpp>
|
||||
#include <godot_cpp/variant/array.hpp>
|
||||
#include <godot_cpp/variant/packed_byte_array.hpp>
|
||||
#include <godot_cpp/variant/packed_vector3_array.hpp>
|
||||
#include <godot_cpp/variant/packed_vector2_array.hpp>
|
||||
#include <godot_cpp/variant/vector2i.hpp>
|
||||
#include <godot_cpp/variant/vector3i.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include "blobber/cell3d.h"
|
||||
}
|
||||
|
||||
using namespace godot;
|
||||
|
||||
namespace {
|
||||
|
||||
/* Match src/gen/grid.h terrain_t / liquid_t values. Kept local so
|
||||
dungeon_to_dict stays independent of gen/grid.h at compile time. */
|
||||
constexpr uint8_t T_NOTHING = 0;
|
||||
constexpr uint8_t T_FLOOR = 1;
|
||||
constexpr uint8_t T_WALL = 2;
|
||||
constexpr uint8_t T_DOOR = 3;
|
||||
constexpr uint8_t T_LIQUID = 5;
|
||||
constexpr uint8_t T_BRIDGE = 6;
|
||||
constexpr uint8_t T_STAIRS_UP = 7;
|
||||
constexpr uint8_t T_STAIRS_DOWN= 8;
|
||||
|
||||
constexpr uint8_t L_NONE = 0;
|
||||
constexpr uint8_t L_WATER = 1;
|
||||
constexpr uint8_t L_LAVA = 2;
|
||||
constexpr uint8_t L_CHASM = 3;
|
||||
|
||||
/* Translate a cell3d_t's floor type into the brogue exporter's
|
||||
(terrain, liquid) pair. Walls are represented by FT_VOID here — the
|
||||
blobber grid surrounds rooms with solid voids rather than emitting a
|
||||
T_WALL-equivalent cell, so VOID is what gives us the wall geometry. */
|
||||
void floor_to_2d(uint8_t b_floor, uint8_t &terrain, uint8_t &liquid) {
|
||||
terrain = T_NOTHING;
|
||||
liquid = L_NONE;
|
||||
switch (b_floor) {
|
||||
case FT_VOID: terrain = T_WALL; break;
|
||||
case FT_STONE: terrain = T_FLOOR; break;
|
||||
case FT_WATER: terrain = T_LIQUID; liquid = L_WATER; break;
|
||||
case FT_LAVA: terrain = T_LIQUID; liquid = L_LAVA; break;
|
||||
case FT_CHASM: terrain = T_LIQUID; liquid = L_CHASM; break;
|
||||
case FT_STAIR_UP: terrain = T_STAIRS_UP; break;
|
||||
case FT_STAIR_DOWN: terrain = T_STAIRS_DOWN; break;
|
||||
case FT_BRIDGE: terrain = T_BRIDGE; break;
|
||||
case FT_DOOR_SILL: terrain = T_DOOR; break;
|
||||
default: terrain = T_NOTHING; break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static Array pack_group_arrays(const mesh_group_t *g) {
|
||||
Array arrays;
|
||||
arrays.resize(Mesh::ARRAY_MAX);
|
||||
|
||||
const size_t n = g->vert_count;
|
||||
PackedVector3Array verts; verts.resize((int)n);
|
||||
PackedVector3Array normals; normals.resize((int)n);
|
||||
PackedVector2Array uvs; uvs.resize((int)n);
|
||||
|
||||
Vector3 *vp = verts.ptrw();
|
||||
Vector3 *np = normals.ptrw();
|
||||
Vector2 *tp = uvs.ptrw();
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const mesh_vertex_t *v = &g->verts[i];
|
||||
vp[i] = Vector3(v->px, v->py, v->pz);
|
||||
np[i] = Vector3(v->nx, v->ny, v->nz);
|
||||
tp[i] = Vector2(v->u, v->v);
|
||||
}
|
||||
arrays[Mesh::ARRAY_VERTEX] = verts;
|
||||
arrays[Mesh::ARRAY_NORMAL] = normals;
|
||||
arrays[Mesh::ARRAY_TEX_UV] = uvs;
|
||||
return arrays;
|
||||
}
|
||||
|
||||
Dictionary godot::dungeon_to_dictionary(const dungeon_t *d, const mesh_build_t *mb,
|
||||
int seed, int depth, int num_levels) {
|
||||
Dictionary out;
|
||||
out["seed"] = seed;
|
||||
out["depth"] = depth;
|
||||
out["num_levels"] = num_levels;
|
||||
out["cell_size"] = mb->cell_size;
|
||||
out["dimensions"] = Vector3i(BL_DCOLS, num_levels, BL_DROWS);
|
||||
|
||||
// Raw voxel cell data for future mutation. Order: z-major (level,y,x).
|
||||
const size_t n_cells = (size_t)num_levels * BL_DROWS * BL_DCOLS;
|
||||
PackedByteArray cells;
|
||||
cells.resize((int)(n_cells * sizeof(cell3d_t)));
|
||||
memcpy(cells.ptrw(), d->cells, n_cells * sizeof(cell3d_t));
|
||||
out["cells"] = cells;
|
||||
|
||||
// Per-material mesh surfaces. Only materials with content are emitted.
|
||||
Array meshes;
|
||||
for (int m = 0; m < MAT_COUNT; m++) {
|
||||
const mesh_group_t *g = &mb->groups[m];
|
||||
if (g->vert_count == 0) continue;
|
||||
Dictionary surf;
|
||||
surf["material"] = m;
|
||||
surf["tri_count"] = (int)(g->vert_count / 3);
|
||||
surf["arrays"] = pack_group_arrays(g);
|
||||
meshes.push_back(surf);
|
||||
}
|
||||
out["meshes"] = meshes;
|
||||
|
||||
// Per-level metadata.
|
||||
Array levels;
|
||||
for (int i = 0; i < d->n_levels; i++) {
|
||||
const b_level_info_t *info = &d->levels[i];
|
||||
Dictionary lvl;
|
||||
lvl["index"] = i;
|
||||
lvl["stairs_up"] = Vector2i(info->stairs_up_x, info->stairs_up_y);
|
||||
lvl["stairs_down"] = Vector2i(info->stairs_down_x, info->stairs_down_y);
|
||||
levels.push_back(lvl);
|
||||
}
|
||||
out["levels"] = levels;
|
||||
|
||||
// Plan reserved for PR 3+; keep the key present so consumers can rely on it.
|
||||
Dictionary plan;
|
||||
plan["stairs"] = Array();
|
||||
plan["chasms"] = Array();
|
||||
plan["waterfalls"] = Array();
|
||||
plan["footprints"] = Array();
|
||||
out["plan"] = plan;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
TypedArray<Dictionary> godot::dungeon_to_2d_slices(const dungeon_t *d) {
|
||||
TypedArray<Dictionary> slices;
|
||||
if (!d) return slices;
|
||||
|
||||
const int w = BL_DCOLS;
|
||||
const int h = BL_DROWS;
|
||||
const int per_level = w * h;
|
||||
|
||||
for (int lvl = 0; lvl < d->n_levels; lvl++) {
|
||||
PackedByteArray terrain; terrain.resize(per_level);
|
||||
PackedByteArray liquid; liquid.resize(per_level);
|
||||
uint8_t *tp = terrain.ptrw();
|
||||
uint8_t *lp = liquid.ptrw();
|
||||
|
||||
const cell3d_t *base = &d->cells[lvl * per_level];
|
||||
for (int i = 0; i < per_level; i++) {
|
||||
uint8_t t, l;
|
||||
floor_to_2d(base[i].floor, t, l);
|
||||
tp[i] = t;
|
||||
lp[i] = l;
|
||||
}
|
||||
|
||||
Dictionary slice;
|
||||
slice["width"] = w;
|
||||
slice["height"] = h;
|
||||
slice["terrain"] = terrain;
|
||||
slice["liquid"] = liquid;
|
||||
slices.push_back(slice);
|
||||
}
|
||||
|
||||
return slices;
|
||||
}
|
||||
28
godot/src/dungeon_to_dict.h
Normal file
28
godot/src/dungeon_to_dict.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <godot_cpp/variant/dictionary.hpp>
|
||||
#include <godot_cpp/variant/typed_array.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include "blobber/dungeon.h"
|
||||
#include "mesh/face_mesh.h"
|
||||
}
|
||||
|
||||
namespace godot {
|
||||
|
||||
/* Pack dungeon_t + mesh_build_t into a Dictionary suitable for return from
|
||||
BrogueGen.generate_dungeon(). Keys: seed, num_levels, dimensions,
|
||||
cell_size, cells (PackedByteArray of raw cell3d_t), meshes
|
||||
(Array of per-material surface arrays), levels (metadata), plan (empty
|
||||
for PR 1). */
|
||||
Dictionary dungeon_to_dictionary(const dungeon_t *d, const mesh_build_t *mb,
|
||||
int seed, int depth, int num_levels);
|
||||
|
||||
/* Flatten a 3D blobber dungeon into an Array of per-level 2D slices shaped
|
||||
like legacy brogue's grid_to_dictionary() output: each entry is a
|
||||
Dictionary with {width, height, terrain: PackedByteArray,
|
||||
liquid: PackedByteArray}. Terrain / liquid values match the T_* / L_*
|
||||
enums in src/gen/grid.h so the existing .map exporter can consume them. */
|
||||
TypedArray<Dictionary> dungeon_to_2d_slices(const dungeon_t *d);
|
||||
|
||||
} // namespace godot
|
||||
Loading…
Add table
Add a link
Reference in a new issue