#!/usr/bin/env python3 """ Generate Godot 4 .tscn modular kit scenes using CSG nodes. All dimensions in Godot meters, grid base = 2.56m (256cm). """ import os SCENES_DIR = os.path.expanduser("/home/oplabs/tactical-shooter/client/assets/scenes/modular") MAT_BASE = "res://assets/materials" # Grid constants U = 2.56 # 1 modular unit (256cm) W = 0.16 # wall thickness (16cm) F = 0.08 # floor thickness (8cm) # Material name shortcuts MAT_WALL = "wall_concrete_01" MAT_WALL2 = "wall_concrete_02" MAT_FLOOR = "floor_concrete_01" MAT_FLOOR_TILE = "floor_tile_01" MAT_METAL = "metal_structural_01" MAT_BLUE = "accent_team_blue" MAT_RED = "accent_team_red" def tsnode(name, type_name, parent=".", props=None): """Build a [node] entry for .tscn format.""" lines = [f'[node name="{name}" type="{type_name}" parent="{parent}"]'] if props: for k, v in props.items(): lines.append(f"{k} = {v}") return "\n".join(lines) # ── Wall pieces ────────────────────────────────────────────────────────── def build_wall_straight(): """Straight wall — 2.56m x 2.56m x 0.16m.""" lines = [ '[gd_scene load_steps=2 format=3]', f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL}.tres" id="1"]', '', tsnode("WallStraight01", "CSGBox3D", ".", { "size": f"Vector3({U}, {U}, {W})", "material": 'ExtResource("1")', "use_collision": "true", }), ] return "\n".join(lines) + "\n" def build_wall_corner(): """L-corner — two CSG boxes in a CSGCombiner3D.""" lines = [ '[gd_scene load_steps=2 format=3]', f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL}.tres" id="1"]', '', tsnode("WallCorner01", "CSGCombiner3D", ".", {"use_collision": "false"}), tsnode("WallA", "CSGBox3D", "WallCorner01", { "operation": "0", "size": f"Vector3({U}, {U}, {W})", "material": 'ExtResource("1")', "use_collision": "true", "transform": f"Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, {W/2})", }), tsnode("WallB", "CSGBox3D", "WallCorner01", { "operation": "0", "size": f"Vector3({W}, {U}, {U})", "material": 'ExtResource("1")', "use_collision": "true", "transform": f"Transform3D(0, 0, -1, 0, 1, 0, 1, 0, 0, {W/2}, 0, 0)", }), ] return "\n".join(lines) + "\n" def build_wall_doorway(): """Wall with door opening (0.80m x 2.00m cutout).""" dw, dh = 0.80, 2.00 off_y = dh/2 - U/2 # bottom-aligned lines = [ '[gd_scene load_steps=2 format=3]', f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL}.tres" id="1"]', '', tsnode("WallDoorway01", "CSGCombiner3D", ".", {"use_collision": "false"}), tsnode("MainWall", "CSGBox3D", "WallDoorway01", { "operation": "0", "size": f"Vector3({U}, {U}, {W})", "material": 'ExtResource("1")', "use_collision": "true", }), tsnode("DoorCutout", "CSGBox3D", "WallDoorway01", { "operation": "1", "size": f"Vector3({dw}, {dh}, {W + 0.04})", "use_collision": "true", "transform": f"Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, {off_y}, 0)", }), ] return "\n".join(lines) + "\n" def build_wall_window(): """Wall with window opening (1.20m x 0.80m cutout, centered slightly above mid).""" ww, wh = 1.20, 0.80 off_y = 0.30 lines = [ '[gd_scene load_steps=2 format=3]', f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL}.tres" id="1"]', '', tsnode("WallWindow01", "CSGCombiner3D", ".", {"use_collision": "false"}), tsnode("MainWall", "CSGBox3D", "WallWindow01", { "operation": "0", "size": f"Vector3({U}, {U}, {W})", "material": 'ExtResource("1")', "use_collision": "true", }), tsnode("WindowCutout", "CSGBox3D", "WallWindow01", { "operation": "1", "size": f"Vector3({ww}, {wh}, {W + 0.04})", "use_collision": "true", "transform": f"Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, {off_y}, 0)", }), ] return "\n".join(lines) + "\n" def build_wall_endcap(): """Narrow edge cap — 0.32m x 2.56m x 0.16m.""" lines = [ '[gd_scene load_steps=2 format=3]', f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL}.tres" id="1"]', '', tsnode("WallEndcap01", "CSGBox3D", ".", { "size": "Vector3(0.32, 2.56, 0.16)", "material": 'ExtResource("1")', "use_collision": "true", }), ] return "\n".join(lines) + "\n" # ── Floor pieces ───────────────────────────────────────────────────────── def build_floor_tile(): """2.56m x 0.08m x 2.56m floor slab — industrial concrete.""" lines = [ f'[gd_scene load_steps=2 format=3]', f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_FLOOR}.tres" id="1"]', '', tsnode("FloorTile01", "CSGBox3D", ".", { "size": f"Vector3({U}, {F}, {U})", "material": 'ExtResource("1")', "use_collision": "true", }), ] return "\n".join(lines) + "\n" def build_floor_tile_ceramic(): """Floor slab with ceramic tile material.""" lines = [ f'[gd_scene load_steps=2 format=3]', f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_FLOOR_TILE}.tres" id="1"]', '', tsnode("FloorTileCeramic01", "CSGBox3D", ".", { "size": f"Vector3({U}, {F}, {U})", "material": 'ExtResource("1")', "use_collision": "true", }), ] return "\n".join(lines) + "\n" # ── Structural ─────────────────────────────────────────────────────────── def build_pillar(): """Square pillar — 0.64m x 2.56m x 0.64m.""" lines = [ f'[gd_scene load_steps=2 format=3]', f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_WALL2}.tres" id="1"]', '', tsnode("Pillar01", "CSGBox3D", ".", { "size": "Vector3(0.64, 2.56, 0.64)", "material": 'ExtResource("1")', "use_collision": "true", }), ] return "\n".join(lines) + "\n" def build_beam(): """Ceiling beam — 3.84m x 0.32m x 0.32m metal.""" lines = [ f'[gd_scene load_steps=2 format=3]', f'[ext_resource type="Material" path="{MAT_BASE}/{MAT_METAL}.tres" id="1"]', '', tsnode("Beam01", "CSGBox3D", ".", { "size": f"Vector3({U * 1.5}, 0.32, 0.32)", "material": 'ExtResource("1")', "use_collision": "true", }), ] return "\n".join(lines) + "\n" # ── Accent panels ──────────────────────────────────────────────────────── def build_accent_panel(name, mat_name, color_label): """A thin accent panel with team color material.""" lines = [ f'[gd_scene load_steps=2 format=3]', f'[ext_resource type="Material" path="{MAT_BASE}/{mat_name}.tres" id="1"]', '', tsnode(f"{color_label}Panel01", "CSGBox3D", ".", { "size": f"Vector3(1.28, 0.64, {W * 0.5})", "material": 'ExtResource("1")', "use_collision": "true", }), ] return "\n".join(lines) + "\n" # ── Kit demo scene ─────────────────────────────────────────────────────── def build_kit_demo(): """Showcase scene — places every modular piece on a 3m-spaced strip.""" # Each entry: (scene_name, ext_material_name, label_for_node) entries = [ ("wall_straight_01", MAT_WALL, "Wall01"), ("wall_corner_01", MAT_WALL, "Corner01"), ("wall_doorway_01", MAT_WALL, "Doorway01"), ("wall_window_01", MAT_WALL, "Window01"), ("wall_endcap_01", MAT_WALL, "Endcap01"), ("floor_tile_01", MAT_FLOOR, "Floor01"), ("floor_tile_ceramic_01", MAT_FLOOR_TILE, "FloorCeramic01"), ("pillar_01", MAT_WALL2, "Pillar01"), ("beam_01", MAT_METAL, "Beam01"), ("accent_blue_panel", MAT_BLUE, "BluePanel01"), ("accent_red_panel", MAT_RED, "RedPanel01"), ] # All unique materials used all_mats = sorted(set(e[1] for e in entries)) mat_scene_map = {} # mat_name -> {scene_name -> ext_id} scene_map = {} # scene_name -> ext_id ext_id_counter = 1 lines = [] # First pass: assign ext ids to scenes (PackedScene) and materials scene_ext_ids = {} mat_ext_ids = {} for scene_name, mat_name, _ in entries: if scene_name not in scene_ext_ids: scene_ext_ids[scene_name] = ext_id_counter ext_id_counter += 1 if mat_name not in mat_ext_ids: mat_ext_ids[mat_name] = ext_id_counter ext_id_counter += 1 total_ext = len(scene_ext_ids) + len(mat_ext_ids) load_steps = total_ext + 1 lines = [f'[gd_scene load_steps={load_steps} format=3]'] # Ext resources: scenes for scene_name, eid in scene_ext_ids.items(): lines.append( f'[ext_resource type="PackedScene" ' f'path="res://assets/scenes/modular/{scene_name}.tscn" id="{eid}"]' ) # Ext resources: materials (for any inline nodes) for mat_name, eid in mat_ext_ids.items(): lines.append( f'[ext_resource type="Material" ' f'path="{MAT_BASE}/{mat_name}.tres" id="{eid}"]' ) lines.append("") lines.append('[node name="KitDemo" type="Node3D"]') lines.append("") x = -5.0 for scene_name, mat_name, label in entries: scene_eid = scene_ext_ids[scene_name] lines.append( f'[node name="{label}" type="Node3D" parent="."]' ) lines.append(f'instance = ExtResource("{scene_eid}")') lines.append(f'transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, {x:.1f}, 0, 0)') lines.append("") x += 3.0 return "\n".join(lines) + "\n" # ── Main ───────────────────────────────────────────────────────────────── SCENE_BUILDERS = { "wall_straight_01": build_wall_straight, "wall_corner_01": build_wall_corner, "wall_doorway_01": build_wall_doorway, "wall_window_01": build_wall_window, "wall_endcap_01": build_wall_endcap, "floor_tile_01": build_floor_tile, "floor_tile_ceramic_01": build_floor_tile_ceramic, "pillar_01": build_pillar, "beam_01": build_beam, "accent_blue_panel": lambda: build_accent_panel("accent_blue_panel", MAT_BLUE, "Blue"), "accent_red_panel": lambda: build_accent_panel("accent_red_panel", MAT_RED, "Red"), "kit_demo": build_kit_demo, } def main(): os.makedirs(SCENES_DIR, exist_ok=True) print("Generating modular .tscn scenes...") for name, builder in SCENE_BUILDERS.items(): content = builder() path = os.path.join(SCENES_DIR, f"{name}.tscn") with open(path, "w") as f: f.write(content) print(f" Created {name}.tscn ({len(content)} bytes)") print(f"\nDone — {len(SCENE_BUILDERS)} .tscn files") if __name__ == "__main__": main()