extends SceneTree # Usage: # godot --headless --path demo --script scripts/export_map.gd -- SEED DEPTH [LEVELS] OUT.map # # Generates N dungeons (seeds SEED..SEED+N-1, depths DEPTH..DEPTH+N-1), # stacks them vertically, and writes one Standard Quake .map file. # # Chasms on non-bottom levels are real holes — no floor brush, and the # level below has its ceiling drilled out at the same (x,y) so the shaft # stays open all the way down until it hits a non-chasm floor. Chasms on # the bottom level get a local pit floor at CHASM_TOP so you don't fall # into the void. const TILE_SIZE := 64 const HEIGHT := 128 const WALL_THICKNESS := 64 const TEXTURE := "__TB_empty" const FLOOR_TOP := 0 const WATER_TOP := -32 const CHASM_TOP := -128 const PIT_BOTTOM := CHASM_TOP - WALL_THICKNESS # = -192 const WALL_TOP := HEIGHT + WALL_THICKNESS # = 192 # Per-level Z offset. Each level's brushes get translated by # -level_index * LEVEL_SPACING. Sized so level N+1's ceiling top sits at # or below level N's PIT_BOTTOM — no overlap, no z-fighting. const LEVEL_SPACING := 384 # terrain_t const T_NOTHING := 0 const T_FLOOR := 1 const T_WALL := 2 const T_DOOR := 3 const T_CORRIDOR := 4 const T_LIQUID := 5 const T_BRIDGE := 6 const T_STAIRS_UP := 7 const T_STAIRS_DOWN := 8 # liquid_t const L_WATER := 1 const L_CHASM := 3 enum Kind { EMPTY, WALL, FLOOR, WATER, CHASM } func _init() -> void: var args := OS.get_cmdline_user_args() var seed: int var depth: int var levels: int var out_path: String match args.size(): 3: seed = int(args[0]) depth = int(args[1]) levels = 1 out_path = args[2] 4: seed = int(args[0]) depth = int(args[1]) levels = int(args[2]) out_path = args[3] _: push_error("usage: -- SEED DEPTH [LEVELS] OUT.map") quit(1) return if levels < 1: push_error("levels must be >= 1") quit(1) return var grids: Array = [] for k in range(levels): var gen := BrogueGen.new() grids.append(gen.generate(seed + k, depth + k)) gen.free() var f := FileAccess.open(out_path, FileAccess.WRITE) if f == null: push_error("cannot open %s for write" % out_path) quit(1) return var brush_count := _write_map(f, grids, seed, depth) f.close() print("wrote %s — %d levels, %d brushes" % [out_path, levels, brush_count]) quit(0) func _write_map(f: FileAccess, grids: Array, seed: int, depth: int) -> int: f.store_string("// Game: FuncGodot\n") f.store_string("// Format: Valve\n") f.store_string("// Generated by brogue-genesis — seed: %d, depth: %d, levels: %d\n" % [ seed, depth, grids.size(), ]) f.store_string("{\n") f.store_string("\"classname\" \"worldspawn\"\n") var count := 0 var w: int = grids[0]["width"] var h: int = grids[0]["height"] # Per-(x,y) flag: "some level above has a chasm here, so our ceiling # is drilled out so the shaft stays open". Accumulates top-down. var chasm_above := PackedByteArray() chasm_above.resize(w * h) for k in range(grids.size()): var grid: Dictionary = grids[k] var is_bottom := (k == grids.size() - 1) var z_offset := -k * LEVEL_SPACING count += _write_level(f, grid, z_offset, chasm_above, is_bottom) # Update chasm_above for the next level using THIS level's chasms. _propagate_chasms(grid, chasm_above) f.store_string("}\n") return count func _write_level(f: FileAccess, grid: Dictionary, z_off: int, chasm_above: PackedByteArray, is_bottom: bool) -> int: var w: int = grid["width"] var h: int = grid["height"] var terrain: PackedByteArray = grid["terrain"] var liquid: PackedByteArray = grid["liquid"] var count := 0 var ts := TILE_SIZE # Pass 1 — floors / walls, row-merged by kind. for gy in range(h): var run_start := 0 var run_kind := _kind(terrain, liquid, w, 0, gy) for gx in range(1, w + 1): var cur: int = Kind.EMPTY if gx < w: cur = _kind(terrain, liquid, w, gx, gy) if cur == run_kind and gx < w: continue var x0 := run_start * ts var x1 := gx * ts var y0 := gy * ts var y1 := (gy + 1) * ts match run_kind: Kind.WALL: count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM, x1, y1, z_off + WALL_TOP) Kind.FLOOR: count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM, x1, y1, z_off + FLOOR_TOP) Kind.WATER: count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM, x1, y1, z_off + WATER_TOP) Kind.CHASM: # Non-bottom chasms are real holes — no floor. Bottom # chasms get a local pit floor so the player doesn't # fall into empty void. if is_bottom: count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM, x1, y1, z_off + CHASM_TOP) _: pass run_start = gx run_kind = cur # Pass 2 — ceilings, row-merged by "is ceiling present here?". A # ceiling is present if the cell has floor-like terrain AND no level # above has drilled a chasm through it. for gy in range(h): var run_start := 0 var run_has := _has_ceiling(terrain, chasm_above, w, 0, gy) for gx in range(1, w + 1): var cur := false if gx < w: cur = _has_ceiling(terrain, chasm_above, w, gx, gy) if cur == run_has and gx < w: continue if run_has: var x0 := run_start * ts var x1 := gx * ts var y0 := gy * ts var y1 := (gy + 1) * ts count += _emit_box(f, x0, y0, z_off + HEIGHT, x1, y1, z_off + WALL_TOP) run_start = gx run_has = cur return count # Record any chasm cells at this level into the running mask so the NEXT # level knows to drill its ceiling there. func _propagate_chasms(grid: Dictionary, chasm_above: PackedByteArray) -> void: var w: int = grid["width"] var h: int = grid["height"] var terrain: PackedByteArray = grid["terrain"] var liquid: PackedByteArray = grid["liquid"] for gy in range(h): for gx in range(w): var idx := gy * w + gx if terrain[idx] == T_LIQUID and liquid[idx] == L_CHASM: chasm_above[idx] = 1 # Draw kind for floor/wall emission. func _kind(terrain: PackedByteArray, liquid: PackedByteArray, w: int, x: int, y: int) -> int: var idx := y * w + x var t: int = terrain[idx] match t: T_NOTHING: return Kind.EMPTY T_WALL: return Kind.WALL T_LIQUID: var liq: int = liquid[idx] if liq == L_CHASM: return Kind.CHASM if liq == L_WATER: return Kind.WATER return Kind.FLOOR # lava / brimstone sit at floor level _: return Kind.FLOOR # Ceiling emitted when the cell is floor-like AND no chasm sits above it. # Walls and empty cells don't get ceilings either (wall brush already # reaches WALL_TOP; empty is empty). func _has_ceiling(terrain: PackedByteArray, chasm_above: PackedByteArray, w: int, x: int, y: int) -> bool: var idx := y * w + x if chasm_above[idx] != 0: return false var t: int = terrain[idx] return t != T_NOTHING and t != T_WALL # Standard Quake brush: 6 axis-aligned planes, 3 points per plane, inward # normals per TrenchBroom convention. Mirrors libd's emit_solid_box. func _emit_box(f: FileAccess, x0: int, y0: int, z0: int, x1: int, y1: int, z1: int) -> int: if x0 >= x1 or y0 >= y1 or z0 >= z1: return 0 f.store_string("{\n") # Valve 220 texture axes per face normal (Quake convention). # X-facing walls: U=Y, V=-Z. Y-facing walls: U=X, V=-Z. Z-facing: U=X, V=-Y. var ax_x := "[ 0 1 0 0 ] [ 0 0 -1 0 ]" var ax_y := "[ 1 0 0 0 ] [ 0 0 -1 0 ]" var ax_z := "[ 1 0 0 0 ] [ 0 -1 0 0 ]" # -X _face(f, x0, y0, z0, x0, y1, z0, x0, y0, z1, ax_x) # +X _face(f, x1, y0, z0, x1, y0, z1, x1, y1, z0, ax_x) # -Y _face(f, x0, y0, z1, x1, y0, z1, x0, y0, z0, ax_y) # +Y _face(f, x0, y1, z0, x1, y1, z0, x0, y1, z1, ax_y) # -Z _face(f, x0, y0, z0, x1, y0, z0, x0, y1, z0, ax_z) # +Z _face(f, x0, y0, z1, x0, y1, z1, x1, y0, z1, ax_z) f.store_string("}\n") return 1 func _face(f: FileAccess, x1: int, y1: int, z1: int, x2: int, y2: int, z2: int, x3: int, y3: int, z3: int, axes: String) -> void: f.store_string("( %d %d %d ) ( %d %d %d ) ( %d %d %d ) %s %s 0 1 1\n" % [ x1, y1, z1, x2, y2, z2, x3, y3, z3, TEXTURE, axes, ])