@tool extends Node # ============================================================================= # art_kit.gd — Modular Art Kit Factory # ============================================================================= # Provides factory methods for spawning modular environment pieces and a # preview catalogue for map builders. # # Each piece is a standalone .tscn under pieces/ with: # - CSG shapes (Box3D, Cylinder3D, Polygon3D, Combiner3D) # - Solid-color StandardMaterial3D placeholders (roughness 0.8, metallic 0.0) # - Collision enabled (use_collision = true or StaticBody3D + CollisionShape3D) # - < 200 tris each # # Usage: # var piece = ArtKit.spawn_piece("crate", parent_node) # var cat = ArtKit.preview_catalogue() # var stack = ArtKit.random_crate_layout(3, 2) # returns parent Node3D # ============================================================================= const PIECES_DIR := "res://client/assets/scenes/modular/pieces/" # Catalogue definition: name, description, filename (without .tscn) const CATALOGUE: Array[Dictionary] = [ { "name": "floor_tile", "description": "2×2×0.2m concrete slab — grey", "file": "floor_tile" }, { "name": "wall_segment", "description": "4×0.2×3m wall panel — blue-grey", "file": "wall_segment" }, { "name": "corner_wall", "description": "L-shaped corner piece (2m each leg) — blue-grey", "file": "corner_wall" }, { "name": "pillar", "description": "0.3×3×0.3m column — dark grey", "file": "pillar" }, { "name": "crate", "description": "1×1×1m storage box — brown/tan", "file": "crate" }, { "name": "barrel", "description": "0.6m radius × 1m tall cylinder — red", "file": "barrel" }, { "name": "cover_wall", "description": "2×0.2×1.3m low wall (crouch-height cover) — grey", "file": "cover_wall" }, { "name": "ramp", "description": "2×2m, 30° incline ramp — yellow", "file": "ramp" }, { "name": "stairs", "description": "3-step staircase (2m wide, 0.25m rise per step) — grey", "file": "stairs" }, { "name": "window_frame", "description": "4×3m wall with 2×2m opening — blue-grey", "file": "window_frame" } ] # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- ## Spawns a piece by name, adds it as a child of `parent`, and returns it. ## Returns null if the piece name is unknown or the scene fails to load. static func spawn_piece(piece_name: String, parent: Node) -> Node3D: var entry := _find_entry(piece_name) if entry.is_empty(): push_error("ArtKit: Unknown piece '%s'. Use preview_catalogue() to see available pieces." % piece_name) return null var path := PIECES_DIR + entry.file + ".tscn" var scene := load(path) if scene == null: push_error("ArtKit: Failed to load scene at '%s'." % path) return null var instance: Node3D = scene.instantiate() parent.add_child(instance) instance.owner = parent if not Engine.is_editor_hint() else _get_editor_owner(parent) return instance ## Returns an array of { name, description, path } for all available pieces. static func preview_catalogue() -> Array[Dictionary]: var result: Array[Dictionary] = [] for entry in CATALOGUE: result.append({ "name": entry.name, "description": entry.description, "path": PIECES_DIR + entry.file + ".tscn" }) return result ## Generates a random stack of crates for quick testing layouts. ## `width` — number of crates along X (default 3) ## `height` — number of crates stacked vertically (default 2) ## Returns a Node3D containing the crate instances. static func random_crate_layout(width: int = 3, height: int = 2) -> Node3D: var root := Node3D.new() root.name = "CrateLayout_%dx%d" % [width, height] var crate_scene := load(PIECES_DIR + "crate.tscn") if crate_scene == null: push_error("ArtKit: Could not load crate scene.") return root for y in range(height): for x in range(width): # Add slight random offset for natural look (±0.05m) var offset_x := randf_range(-0.05, 0.05) var offset_z := randf_range(-0.05, 0.05) # Full rotation in 90° increments var rot_y := randi() % 4 * 90.0 var crate: Node3D = crate_scene.instantiate() crate.position = Vector3(x * 1.0 + offset_x, y * 1.0 + 0.5, offset_z) crate.rotation_degrees = Vector3(0, rot_y, 0) root.add_child(crate) return root # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- static func _find_entry(piece_name: String) -> Dictionary: for entry in CATALOGUE: if entry.name == piece_name: return entry return {} ## Returns the editor's top-level owner so new nodes are treated as part of the scene. static func _get_editor_owner(node: Node) -> Node: var owner = node while owner and owner.owner: owner = owner.owner return owner