class_name MeshLibraryBuilder extends RefCounted # Builds a MeshLibrary at runtime for the 3D dungeon demo. No art assets — # BoxMesh / PlaneMesh primitives with tinted StandardMaterial3D. Each tile # ID here matches a terrain enum the renderer cares about. # Tile IDs (exposed as constants so demo_3d.gd can pass them to # GridMap.set_cell_item). const TILE_FLOOR := 0 const TILE_WALL := 1 const TILE_DOOR := 2 const TILE_CORRIDOR := 3 const TILE_WATER := 4 const TILE_LAVA := 5 const TILE_BRIMSTONE := 6 const TILE_BRIDGE := 7 const TILE_STAIRS_UP := 8 const TILE_STAIRS_DOWN := 9 # Floors and liquid surfaces are thin slabs centered at the cell origin. # Walls are tall enough that the player (camera ~2.6u) can't see over them. # Doors are chest-height so they read as entry markers without blocking view # along corridors — can raise to full height once we have swinging doors. const CELL_SIZE := 1.0 const WALL_HEIGHT := 3.0 const FLOOR_THICKNESS := 0.05 const LIQUID_THICKNESS := 0.05 const DOOR_HEIGHT := 0.8 const BRIDGE_THICKNESS := 0.1 const STAIRS_HEIGHT := 0.7 static func build(with_collision: bool = true) -> MeshLibrary: var lib := MeshLibrary.new() _add_slab(lib, TILE_FLOOR, "Floor", _mat(Color(0.64, 0.54, 0.42)), FLOOR_THICKNESS, 0.0) _add_cube(lib, TILE_WALL, "Wall", _mat(Color(0.28, 0.28, 0.32)), WALL_HEIGHT, WALL_HEIGHT * 0.5) _add_cube(lib, TILE_DOOR, "Door", _mat(Color(0.72, 0.36, 0.20)), DOOR_HEIGHT, DOOR_HEIGHT * 0.5) _add_slab(lib, TILE_CORRIDOR, "Corridor", _mat(Color(0.44, 0.37, 0.29)), FLOOR_THICKNESS, 0.0) _add_slab(lib, TILE_WATER, "Water", _mat_trans(Color(0.18, 0.42, 0.78, 0.75)), LIQUID_THICKNESS, -0.05) _add_slab(lib, TILE_LAVA, "Lava", _mat_emissive(Color(0.90, 0.30, 0.12), Color(1.00, 0.45, 0.15), 1.5), LIQUID_THICKNESS, -0.05) _add_slab(lib, TILE_BRIMSTONE, "Brimstone", _mat_emissive(Color(0.72, 0.50, 0.22), Color(0.95, 0.65, 0.25), 0.8), LIQUID_THICKNESS, -0.05) _add_slab(lib, TILE_BRIDGE, "Bridge", _mat(Color(0.55, 0.38, 0.22)), BRIDGE_THICKNESS, 0.0) _add_ramp(lib, TILE_STAIRS_UP, "StairsUp", _mat(Color(0.74, 0.88, 0.94)), true) _add_ramp(lib, TILE_STAIRS_DOWN, "StairsDown", _mat(Color(0.20, 0.44, 0.80)), false) if with_collision: _attach_collision(lib) return lib # Bake the library to a .tres so the CLI and runtime can share the same asset # instead of rebuilding it each time a scene loads. static func save_resource(path: String, with_collision: bool = true) -> Error: return ResourceSaver.save(build(with_collision), path) # --- collision shapes --- # Every tile except chasm (which has no tile at all) gets a box collider # sized to match its mesh. Chasms therefore let the player fall through # cleanly onto the level below. Stairs are treated as solid blocks; walking # onto them works well enough with gravity + move_and_slide. static func _attach_collision(lib: MeshLibrary) -> void: # Shape, y_offset pairs, in the same geometry as the meshes above. var floor_shape := _box_shape(Vector3(CELL_SIZE, FLOOR_THICKNESS, CELL_SIZE)) var wall_shape := _box_shape(Vector3(CELL_SIZE, WALL_HEIGHT, CELL_SIZE)) var door_shape := _box_shape(Vector3(CELL_SIZE, DOOR_HEIGHT, CELL_SIZE)) var liquid_shape := _box_shape(Vector3(CELL_SIZE, LIQUID_THICKNESS, CELL_SIZE)) var bridge_shape := _box_shape(Vector3(CELL_SIZE, BRIDGE_THICKNESS, CELL_SIZE)) var stairs_shape := _box_shape(Vector3(CELL_SIZE, STAIRS_HEIGHT, CELL_SIZE)) _set_shape(lib, TILE_FLOOR, floor_shape, FLOOR_THICKNESS * 0.5) _set_shape(lib, TILE_WALL, wall_shape, WALL_HEIGHT * 0.5) _set_shape(lib, TILE_DOOR, door_shape, DOOR_HEIGHT * 0.5) _set_shape(lib, TILE_CORRIDOR, floor_shape, FLOOR_THICKNESS * 0.5) _set_shape(lib, TILE_WATER, liquid_shape, -0.05 + LIQUID_THICKNESS * 0.5) _set_shape(lib, TILE_LAVA, liquid_shape, -0.05 + LIQUID_THICKNESS * 0.5) _set_shape(lib, TILE_BRIMSTONE, liquid_shape, -0.05 + LIQUID_THICKNESS * 0.5) _set_shape(lib, TILE_BRIDGE, bridge_shape, BRIDGE_THICKNESS * 0.5) _set_shape(lib, TILE_STAIRS_UP, stairs_shape, STAIRS_HEIGHT * 0.5) _set_shape(lib, TILE_STAIRS_DOWN, stairs_shape, STAIRS_HEIGHT * 0.5) static func _box_shape(size: Vector3) -> BoxShape3D: var b := BoxShape3D.new() b.size = size return b static func _set_shape(lib: MeshLibrary, id: int, shape: Shape3D, y: float) -> void: var t := Transform3D.IDENTITY t.origin = Vector3(0, y, 0) lib.set_item_shapes(id, [shape, t]) # --- material helpers --- static func _mat(base: Color) -> StandardMaterial3D: var m := StandardMaterial3D.new() m.albedo_color = base m.roughness = 0.8 m.metallic = 0.0 return m static func _mat_trans(base: Color) -> StandardMaterial3D: var m := _mat(base) m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA return m static func _mat_emissive(base: Color, emis: Color, strength: float) -> StandardMaterial3D: var m := _mat(base) m.emission_enabled = true m.emission = emis m.emission_energy_multiplier = strength return m # --- mesh builders --- # A flat slab filling the cell's XZ footprint, with the given thickness and # base at y_base (the slab extends UP from y_base by `thickness`). static func _add_slab(lib: MeshLibrary, id: int, name: String, mat: StandardMaterial3D, thickness: float, y_base: float) -> void: var mesh := BoxMesh.new() mesh.size = Vector3(CELL_SIZE, thickness, CELL_SIZE) mesh.material = mat var t := Transform3D.IDENTITY t.origin = Vector3(0, y_base + thickness * 0.5, 0) _install(lib, id, name, mesh, t) # A cube centered on the cell. y_center is the Y of the cube's center. static func _add_cube(lib: MeshLibrary, id: int, name: String, mat: StandardMaterial3D, height: float, y_center: float) -> void: var mesh := BoxMesh.new() mesh.size = Vector3(CELL_SIZE, height, CELL_SIZE) mesh.material = mat var t := Transform3D.IDENTITY t.origin = Vector3(0, y_center, 0) _install(lib, id, name, mesh, t) # Simple visual ramp using two cubes at different heights. Not geometrically # correct stairs — just enough to read as "stairs" from flight. # going_up=true means the tall step is at +Z; false means the tall step is # at -Z (visually going down). static func _add_ramp(lib: MeshLibrary, id: int, name: String, mat: StandardMaterial3D, going_up: bool) -> void: # GridMap.set_mesh expects a single Mesh. Fake the steps by stacking two # BoxMeshes into an ArrayMesh via SurfaceTool. var st := SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) st.set_material(mat) var tall := STAIRS_HEIGHT var short := STAIRS_HEIGHT * 0.35 var half := CELL_SIZE * 0.25 # Step 1: back (taller going up, shorter going down). var z_back := half if going_up else -half _append_box(st, Vector3(0, (tall if going_up else short) * 0.5, z_back), Vector3(CELL_SIZE, (tall if going_up else short), CELL_SIZE * 0.5)) # Step 2: front. var z_front := -half if going_up else half _append_box(st, Vector3(0, (short if going_up else tall) * 0.5, z_front), Vector3(CELL_SIZE, (short if going_up else tall), CELL_SIZE * 0.5)) var arr := st.commit() _install(lib, id, name, arr, Transform3D.IDENTITY) static func _append_box(st: SurfaceTool, center: Vector3, size: Vector3) -> void: var h := size * 0.5 var c := center var v := [ c + Vector3(-h.x, -h.y, -h.z), # 0 c + Vector3( h.x, -h.y, -h.z), # 1 c + Vector3( h.x, h.y, -h.z), # 2 c + Vector3(-h.x, h.y, -h.z), # 3 c + Vector3(-h.x, -h.y, h.z), # 4 c + Vector3( h.x, -h.y, h.z), # 5 c + Vector3( h.x, h.y, h.z), # 6 c + Vector3(-h.x, h.y, h.z), # 7 ] # 6 faces, each 2 tris, wound CCW with outward normals. var faces := [ [0, 1, 2, 3, Vector3(0, 0, -1)], [5, 4, 7, 6, Vector3(0, 0, 1)], [1, 5, 6, 2, Vector3( 1, 0, 0)], [4, 0, 3, 7, Vector3(-1, 0, 0)], [3, 2, 6, 7, Vector3(0, 1, 0)], [4, 5, 1, 0, Vector3(0, -1, 0)], ] for f in faces: var n: Vector3 = f[4] for i in [0, 1, 2, 0, 2, 3]: st.set_normal(n) st.add_vertex(v[f[i]]) # Register a mesh + transform with the library under the given ID. static func _install(lib: MeshLibrary, id: int, name: String, mesh: Mesh, t: Transform3D) -> void: lib.create_item(id) lib.set_item_name(id, name) lib.set_item_mesh(id, mesh) lib.set_item_mesh_transform(id, t)