class_name WorldBuilder ## Procedurally assembles a minimal Hollywood Boulevard entrance prototype: const Materials := preload("res://src/game/materials.gd") const T_BRICK := preload("res://assets/textures/ai/game_ready/brick_facade.png") const T_CONCRETE := preload("res://assets/textures/ai/game_ready/concrete.png") const T_ASPHALT := preload("res://assets/textures/ai/game_ready/asphalt.png") const T_AWNING := preload("res://assets/textures/ai/game_ready/awning.png") const T_SKYLINE := preload("res://assets/textures/skyline.png") ## steps, building facade, restricted door, sidewalk, pavement, and street edge. ## Surfaces use the shared PBR profiles in `src/game/materials.gd`; street ## furniture comes from the generated GLB props (tools/model_pipeline.py) with a ## procedural fallback so a headless checkout without imports still builds. ## Generated prop models, one `.glb` per street-furniture piece. const MODEL_DIR := "res://assets/models/generated/" const MODEL_PROPS := ["bollard", "bench", "trash_bin", "planter_box", "newspaper_box", "fire_hydrant"] var root: Node3D = null ## Door lock indicator mesh near the handle; toggled by GameManager. var door_indicator: MeshInstance3D = null var _door_indicator_mat: StandardMaterial3D = null ## Street lamps and signage so night mode can switch them on. var _street_lamps: Array[MeshInstance3D] = [] var _lamp_omni: Array[OmniLight3D] = [] var _lamp_bulb_mats: Array[StandardMaterial3D] = [] var marquee_label: Label3D = null var _marquee_emissive: Array[StandardMaterial3D] = [] ## Facade windows that light up after dark. var _window_mats: Array[StandardMaterial3D] = [] ## Names of the generated props that actually loaded (procedural fallbacks are ## not listed), so tests and the debug overlay can report real model usage. var loaded_models: Array[String] = [] var fallback_props: Array[String] = [] func _init(root_node: Node3D) -> void: root = root_node func build() -> void: _build_ground() _build_building() _build_skyline_backdrop() _build_marquee() _build_steps() _build_sidewalk() _build_street() _build_crosswalk() _build_street_furniture() _build_street_detail() _build_guard_spawn() _build_navmesh() set_night_mode(false) func _add_box(parent: Node3D, size: Vector3, pos: Vector3, color: Color, cast_shadow: bool = false, texture: Texture2D = null, uv_scale: Vector2 = Vector2(1, 1), role: String = "concrete") -> MeshInstance3D: var mesh := BoxMesh.new() mesh.size = size var mat: StandardMaterial3D if texture: mat = Materials.textured(texture, color, uv_scale, role) else: mat = Materials.flat(color, role) mesh.material = mat var mi := MeshInstance3D.new() mi.mesh = mesh mi.position = pos mi.cast_shadow = (1 if cast_shadow else 0) as GeometryInstance3D.ShadowCastingSetting parent.add_child(mi) return mi func _build_ground() -> void: var ground := StaticBody3D.new() ground.name = "Ground" # Large enough that the ground meets the distant skyline ring (no gap of sky # under the buildings when looking down the boulevard). _add_box(root, Vector3(500, 0.5, 500), Vector3(0, -0.25, 20), Color(0.22, 0.24, 0.26), false, null, Vector2.ONE, "asphalt") var col := BoxShape3D.new() col.size = Vector3(500, 1, 500) var shape_owner := ground.create_shape_owner(ground) ground.shape_owner_add_shape(shape_owner, col) root.add_child(ground) func _build_building() -> void: var building := StaticBody3D.new() building.name = "Building" # Main facade (textured brick) _add_box(building, Vector3(40, 18, 4), Vector3(0, 9, -6), Color(0.80, 0.76, 0.74), false, T_BRICK, Vector2(22, 10), "brick") # Stone base course so the facade has a footing instead of meeting the steps dead-on. _add_box(building, Vector3(40, 0.8, 0.35), Vector3(0, 0.4, -3.85), Color(0.58, 0.56, 0.53), false, null, Vector2.ONE, "stone") # Entrance recess + door recess frame _add_box(building, Vector3(5.4, 6.4, 0.2), Vector3(0, 3.2, -3.9), Color(0.08, 0.08, 0.09), false, null, Vector2.ONE, "stone") # Door frame columns and lintel, so the entrance is framed rather than painted on. for side in [-1.55, 1.55]: _add_box(building, Vector3(0.28, 5.2, 0.42), Vector3(side, 2.9, -3.86), Color(0.30, 0.28, 0.26), true, null, Vector2.ONE, "stone") _add_box(building, Vector3(3.4, 0.3, 0.42), Vector3(0, 5.55, -3.86), Color(0.30, 0.28, 0.26), true, null, Vector2.ONE, "stone") # Door frame and dark door _add_box(building, Vector3(3.0, 5.2, 0.3), Vector3(0, 2.9, -3.98), Color(0.13, 0.12, 0.11), true, null, Vector2.ONE, "painted_metal") # Door glass (semi-transparent) var glass := _add_box(building, Vector3(1.8, 1.4, 0.34), Vector3(0, 3.8, -3.9), Color(0.6, 0.75, 0.9, 0.55)) glass.material_override = Materials.glass(Color(0.55, 0.7, 0.88, 0.55)) # Door handles: two slim metal bars where the guard reaches for the lock. for side in [-0.42, 0.42]: _add_box(building, Vector3(0.06, 0.5, 0.06), Vector3(side, 2.6, -3.79), Color(0.75, 0.74, 0.70), false, null, Vector2.ONE, "metal") # Door lock indicator (green = open, red = locked), placed beside the handle door_indicator = _add_box(building, Vector3(0.18, 0.18, 0.24), Vector3(-1.8, 1.7, -3.82), Color(0.1, 0.9, 0.3)) door_indicator.material_override = _lock_indicator_material(Color(0.1, 0.9, 0.3)) _build_facade_windows(building) var col := BoxShape3D.new() col.size = Vector3(40, 18, 4) var so := building.create_shape_owner(building) building.shape_owner_add_shape(so, col) building.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, 9, -6))) root.add_child(building) ## Regular window bands above the entrance. Panes are dark glass by day and ## light up after dark, which gives the facade depth at night. func _build_facade_windows(parent: Node3D) -> void: var columns := [-17.0, -13.6, -10.2, -6.8, 6.8, 10.2, 13.6, 17.0] for y in [9.2, 12.4]: for x in columns: # Recessed reveal, then the pane: gives the window a dark frame edge. _add_box(parent, Vector3(1.96, 2.46, 0.1), Vector3(x, y, -3.99), Color(0.22, 0.21, 0.20), false, null, Vector2.ONE, "stone") var pane := _add_box(parent, Vector3(1.7, 2.2, 0.12), Vector3(x, y, -3.94), Color(1, 1, 1)) var pane_mat := Materials.window_pane() pane.material_override = pane_mat _window_mats.append(pane_mat) # Sill below the frame. _add_box(parent, Vector3(2.1, 0.12, 0.3), Vector3(x, y - 1.3, -3.86), Color(0.55, 0.53, 0.50), false, null, Vector2.ONE, "stone") func _build_steps() -> void: # Steps have collision so NPCs and the guard can walk up the entrance. # The ground and building provide the actual collision boundaries. var steps := StaticBody3D.new() steps.name = "Steps" var step_w := 8.0 var tread := 0.6 var height := 0.16 for i in 4: var y: float = i * height var z: float = 4.0 * tread - i * tread var w: float = step_w - i * 0.35 _add_box(steps, Vector3(w, height, tread), Vector3(0, y + height / 2.0, z), Color(0.55, 0.52, 0.5)) # Add collision shape to each tread var tread_col := BoxShape3D.new() tread_col.size = Vector3(w, height, tread) var owner_id := steps.create_shape_owner(steps) steps.shape_owner_add_shape(owner_id, tread_col) steps.shape_owner_set_transform(owner_id, Transform3D(Basis(), Vector3(0, y + height / 2.0, z))) # Landing at the top with collision var landing_y: float = 4 * height + 0.1 _add_box(steps, Vector3(9, 0.2, 3), Vector3(0, landing_y, -1.5), Color(0.5, 0.47, 0.45)) var landing_col := BoxShape3D.new() landing_col.size = Vector3(9, 0.2, 3) var landing_owner := steps.create_shape_owner(steps) steps.shape_owner_add_shape(landing_owner, landing_col) steps.shape_owner_set_transform(landing_owner, Transform3D(Basis(), Vector3(0, landing_y, -1.5))) root.add_child(steps) func _build_sidewalk() -> void: var sidewalk := StaticBody3D.new() sidewalk.name = "Sidewalk" _add_box(sidewalk, Vector3(30, 0.18, 8), Vector3(0, 0.09, 8), Color(0.74, 0.74, 0.72), false, T_CONCRETE, Vector2(9, 2.4), "concrete") var col := BoxShape3D.new() col.size = Vector3(30, 0.36, 8) var so := sidewalk.create_shape_owner(sidewalk) sidewalk.shape_owner_add_shape(so, col) sidewalk.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, 0, 8))) root.add_child(sidewalk) func _build_street() -> void: var street := StaticBody3D.new() street.name = "Street" # The roadway reads darker than the sidewalk; the raw asphalt texture tinted # white looked like more pavement. _add_box(street, Vector3(120, 0.1, 26), Vector3(0, 0.05, 22), Color(0.52, 0.53, 0.57), false, T_ASPHALT, Vector2(30, 7), "asphalt") # Center dashes along the street (x varied so they form a lane line, not a stack). for i in range(-40, 41, 4): _add_box(street, Vector3(0.2, 0.02, 1.2), Vector3(float(i), 0.11, 22), Color(0.85, 0.85, 0.30), false, null, Vector2.ONE, "concrete") # Curb between the sidewalk and the roadway. _add_box(street, Vector3(120, 0.22, 0.5), Vector3(0, 0.11, 12.1), Color(0.62, 0.61, 0.58), false, null, Vector2.ONE, "concrete") var col := BoxShape3D.new() col.size = Vector3(120, 0.2, 26) var so := street.create_shape_owner(street) street.shape_owner_add_shape(so, col) street.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, 0, 22))) root.add_child(street) func _build_navmesh() -> void: ## Flat navigation mesh over the walkable floor: landing strip, sidewalk, ## and street front. Hand-authored polygons; no runtime bake needed. var region := NavigationRegion3D.new() region.name = "NavRegion" var mesh := NavigationMesh.new() mesh.agent_radius = 0.4 mesh.agent_max_climb = 0.8 mesh.agent_max_slope = 45.0 mesh.cell_size = 0.25 var verts := PackedVector3Array([ Vector3(-18, 0.0, -0.6), Vector3(18, 0.0, -0.6), Vector3(18, 0.0, 20.0), Vector3(-18, 0.0, 20.0), ]) mesh.set_vertices(verts) mesh.add_polygon(PackedInt32Array([0, 1, 2, 3])) region.navigation_mesh = mesh root.add_child(region) func _build_skyline_backdrop() -> void: ## Distant skyline surrounding the block. The texture's sky is transparent ## (alpha scissor), so only building silhouettes show. Four planes form a ## ring: a single plane placed behind the building reads as a floating black ## band from the sidewalk. var backdrop := Node3D.new() backdrop.name = "SkylineBackdrop" var mat := StandardMaterial3D.new() mat.albedo_texture = T_SKYLINE mat.texture_repeat = true mat.uv1_scale = Vector3(3.0, 1.0, 1.0) mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR mat.alpha_scissor_threshold = 0.5 mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.cull_mode = BaseMaterial3D.CULL_DISABLED # Unshaded silhouettes read as pure black at 255-scale; lift the albedo so the # distant skyline sits closer to a hazy daytime city. mat.albedo_color = Color(2.6, 2.7, 3.0, 1.0) _skyline_mat = mat var mesh := PlaneMesh.new() # PlaneMesh defaults to a horizontal (XZ) plane; the skyline must stand up. mesh.orientation = PlaneMesh.FACE_Z mesh.size = Vector2(400, 60) # Far enough that the skyline reads as background and its top edge stays out # of the middle of the sky. const DIST := 200.0 # The texture keeps its buildings in the lower part of the image, so the plane # center sits at half its height: the building bases meet the ground. const CENTER_Y := 30.0 var placements := [ [Vector3(0, CENTER_Y, -DIST), 0.0], [Vector3(0, CENTER_Y, DIST), 0.0], [Vector3(-DIST, CENTER_Y, 0), PI / 2.0], [Vector3(DIST, CENTER_Y, 0), PI / 2.0], ] for placement in placements: var plane := MeshInstance3D.new() plane.mesh = mesh plane.material_override = mat plane.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF plane.position = placement[0] plane.rotation.y = placement[1] backdrop.add_child(plane) root.add_child(backdrop) var _skyline_mat: StandardMaterial3D = null func _build_marquee() -> void: ## Striped awning above the door, then the neon sign strip + label. var awning := _add_box(root, Vector3(10.5, 0.08, 1.8), Vector3(0, 6.55, -3.7), Color(1, 1, 1), true, T_AWNING, Vector2(4, 1), "awning") awning.name = "Awning" ## Neon-style marquee strip above the door: emissive bars + a label. var strip_parent := Node3D.new() strip_parent.name = "Marquee" for offset in [-2.2, 2.2]: var bar := _add_box(strip_parent, Vector3(0.25, 0.5, 0.5), Vector3(offset, 7.4, -3.6), Color(1, 0.35, 0.75)) var mat := _emissive_material(Color(1.0, 0.35, 0.75)) bar.material_override = mat _marquee_emissive.append(mat) marquee_label = Label3D.new() marquee_label.name = "MarqueeLabel" marquee_label.text = "STEP LINE" marquee_label.position = Vector3(0, 7.3, -3.55) marquee_label.modulate = Color(1, 0.6, 0.9) marquee_label.font_size = 160 marquee_label.pixel_size = 0.004 marquee_label.outline_size = 24 marquee_label.outline_modulate = Color(0.9, 0.15, 0.5) marquee_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED marquee_label.no_depth_test = false strip_parent.add_child(marquee_label) root.add_child(strip_parent) func _build_street_furniture() -> void: # Street lamps along the sidewalk (bulbs + omni lights, off by day). for side in [-7.5, 7.5]: var lamp := Node3D.new() lamp.name = "Lamp_%d" % int(side) _add_box(lamp, Vector3(0.22, 5.0, 0.22), Vector3(side, 2.5, 8.5), Color(0.16, 0.16, 0.18), true, null, Vector2.ONE, "metal") _add_box(lamp, Vector3(1.6, 0.15, 0.15), Vector3(side + (0.8 if side < 0 else -0.8), 4.9, 8.5), Color(0.16, 0.16, 0.18), true, null, Vector2.ONE, "metal") var bulb := _add_box(lamp, Vector3(0.16, 0.16, 0.5), Vector3(side + (0.8 if side < 0 else -0.8), 4.85, 8.5), Color(1, 1, 0.85)) var bulb_mat := _emissive_material(Color(1.0, 1.0, 0.8)) bulb.material_override = bulb_mat bulb.material_override.emission_energy_multiplier = 1.5 _lamp_bulb_mats.append(bulb_mat) var omni := OmniLight3D.new() omni.position = Vector3(side + (0.8 if side < 0 else -0.8), 4.7, 8.5) omni.omni_range = 12.0 omni.light_energy = 0.0 omni.light_color = Color(1, 0.93, 0.75) lamp.add_child(omni) _lamp_omni.append(omni) root.add_child(lamp) _street_lamps.append(bulb) # Planters beside the steps were flat boxes that clipped the treads; the # generated planter_box props replace them in _build_street_detail(). # Low-poly trees along the street front. var trunk_color := Color(0.35, 0.25, 0.18) var leaf_color := Color(0.16, 0.42, 0.2) # Flank the walkway; keep the sight line to the door clear. for i in 6: var side := -1.0 if i % 2 == 0 else 1.0 var x := side * (6.5 + float(int(i / 2)) * 3.0) var z := 4.0 + float((i * 7) % 5) var tree := Node3D.new() tree.name = "Tree_%d" % (i + 1) var trunk := MeshInstance3D.new() var trunk_mesh := CylinderMesh.new() trunk_mesh.top_radius = 0.09 trunk_mesh.bottom_radius = 0.14 trunk_mesh.height = 1.4 trunk.mesh = trunk_mesh trunk.material_override = _plain_material(trunk_color, "wood") trunk.position = Vector3(x, 0.7, z) tree.add_child(trunk) var crown := MeshInstance3D.new() var crown_mesh := CylinderMesh.new() crown_mesh.top_radius = 0.05 crown_mesh.bottom_radius = 0.85 crown_mesh.height = 2.2 crown.mesh = crown_mesh crown.material_override = _plain_material(leaf_color, "foliage") crown.position = Vector3(x, 2.1, z) tree.add_child(crown) root.add_child(tree) ## Street furniture built from the generated GLB props (tools/model_pipeline.py). ## The meshes are real 3D models (bollards, benches, bins, planters, newspaper ## boxes, a hydrant) rather than scaled boxes, and every placement is fixed so ## the street looks the same on every run. func _build_street_detail() -> void: var detail := Node3D.new() detail.name = "StreetDetail" root.add_child(detail) # Bollards flank the foot of the steps. for x in [-5.6, -4.2, 4.2, 5.6]: _add_prop_model(detail, "bollard", Vector3(x, 0.18, 4.7)) # Benches face the boulevard. _add_prop_model(detail, "bench", Vector3(-8.8, 0.18, 6.4), 180.0) _add_prop_model(detail, "bench", Vector3(8.8, 0.18, 6.4), 180.0) # Bins where people leave the steps. _add_prop_model(detail, "trash_bin", Vector3(-6.6, 0.18, 4.9)) _add_prop_model(detail, "trash_bin", Vector3(6.6, 0.18, 4.9)) # Planters break up the long sidewalk run. _add_prop_model(detail, "planter_box", Vector3(-11.8, 0.18, 8.8)) _add_prop_model(detail, "planter_box", Vector3(11.8, 0.18, 8.8)) # Newspaper boxes and a hydrant along the curb. _add_prop_model(detail, "newspaper_box", Vector3(-9.6, 0.18, 11.0), -12.0) _add_prop_model(detail, "newspaper_box", Vector3(-8.9, 0.18, 11.0), -12.0) _add_prop_model(detail, "fire_hydrant", Vector3(8.4, 0.18, 11.2)) ## Place one generated prop. Falls back to simple primitives when the `.glb` is ## missing (fresh checkout before `godot --import`) so the world still builds. func _add_prop_model(parent: Node3D, prop: String, pos: Vector3, rot_y_deg: float = 0.0) -> Node3D: var holder := Node3D.new() holder.name = "Prop_%s_%d" % [prop, parent.get_child_count()] holder.position = pos holder.rotation_degrees.y = rot_y_deg var model := _load_prop_scene(prop) if model != null: holder.add_child(model) loaded_models.append(prop) else: holder.add_child(_fallback_prop(prop)) fallback_props.append(prop) parent.add_child(holder) return holder func _load_prop_scene(prop: String) -> Node3D: var path := MODEL_DIR + prop + ".glb" if not ResourceLoader.exists(path): return null var res: Resource = ResourceLoader.load(path) if res is PackedScene: var node: Node = (res as PackedScene).instantiate() if node is Node3D: return node as Node3D node.queue_free() return null ## Rough primitive stand-in per prop, used only when the model is unavailable. func _fallback_prop(prop: String) -> Node3D: var node := Node3D.new() match prop: "bollard": _add_cylinder_mesh(node, 0.09, 0.92, Vector3(0, 0.46, 0), Color(0.16, 0.17, 0.19), "metal") "bench": _add_box(node, Vector3(1.8, 0.08, 0.5), Vector3(0, 0.46, 0), Color(0.42, 0.28, 0.17), true, null, Vector2.ONE, "wood") _add_box(node, Vector3(1.8, 0.42, 0.07), Vector3(0, 0.72, -0.22), Color(0.42, 0.28, 0.17), true, null, Vector2.ONE, "wood") for x in [-0.72, 0.72]: _add_box(node, Vector3(0.09, 0.46, 0.46), Vector3(x, 0.23, 0), Color(0.16, 0.17, 0.19), true, null, Vector2.ONE, "metal") "trash_bin": _add_cylinder_mesh(node, 0.27, 0.78, Vector3(0, 0.39, 0), Color(0.22, 0.30, 0.24), "painted_metal") _add_cylinder_mesh(node, 0.29, 0.06, Vector3(0, 0.81, 0), Color(0.16, 0.17, 0.19), "metal") "planter_box": _add_box(node, Vector3(0.96, 0.54, 0.96), Vector3(0, 0.27, 0), Color(0.52, 0.51, 0.48), true, null, Vector2.ONE, "stone") _add_cylinder_mesh(node, 0.30, 0.55, Vector3(0, 0.86, 0), Color(0.18, 0.42, 0.20), "foliage") "newspaper_box": _add_box(node, Vector3(0.42, 0.72, 0.34), Vector3(0, 0.49, 0), Color(0.16, 0.27, 0.45), true, null, Vector2.ONE, "painted_metal") _add_box(node, Vector3(0.44, 0.10, 0.36), Vector3(0, 0.90, 0), Color(0.85, 0.72, 0.28), true, null, Vector2.ONE, "painted_metal") "fire_hydrant": _add_cylinder_mesh(node, 0.15, 0.62, Vector3(0, 0.31, 0), Color(0.6, 0.12, 0.12), "painted_metal") _add_box(node, Vector3(0.52, 0.10, 0.12), Vector3(0, 0.42, 0), Color(0.75, 0.74, 0.70), true, null, Vector2.ONE, "metal") _: _add_box(node, Vector3(0.5, 0.5, 0.5), Vector3(0, 0.25, 0), Color(0.5, 0.5, 0.5)) return node func _add_cylinder_mesh(parent: Node3D, radius: float, height: float, pos: Vector3, color: Color, role: String = "metal") -> MeshInstance3D: var mi := MeshInstance3D.new() var mesh := CylinderMesh.new() mesh.top_radius = radius mesh.bottom_radius = radius mesh.height = height mesh.radial_segments = 12 mi.mesh = mesh mi.position = pos mi.material_override = Materials.flat(color, role) mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON parent.add_child(mi) return mi func _build_crosswalk() -> void: # White crosswalk stripes in front of the entrance on the street. for i in 8: _add_box(root, Vector3(0.8, 0.02, 0.5), Vector3(-2.8 + i * 0.85, 0.11, 17.0), Color(0.92, 0.92, 0.92)) _add_box(root, Vector3(0.8, 0.02, 0.5), Vector3(-2.8 + i * 0.85, 0.11, 19.0), Color(0.92, 0.92, 0.92)) ## Switch street lamps and signage to night mode (emissive + lights on). func set_night_mode(night: bool) -> void: for mat in _lamp_bulb_mats: mat.emission_energy_multiplier = (2.0 if night else 0.0) for omni in _lamp_omni: # Two lamps at full energy washed the whole sidewalk grey; keep visible # pools of light instead of flat night-time brightness. omni.light_energy = (1.8 if night else 0.0) for mat in _marquee_emissive: mat.emission_energy_multiplier = (2.0 if night else 0.2) # Light most, not all, of the windows: a fully lit grid reads as a light box # instead of an occupied building. for i in _window_mats.size(): var lit := night and i % 3 != 2 _window_mats[i].emission_energy_multiplier = (1.1 if lit else 0.0) if marquee_label: marquee_label.modulate.a = (1.0 if night else 0.9) func _emissive_material(color: Color) -> StandardMaterial3D: return Materials.emissive(color) func _plain_material(color: Color, role: String = "concrete") -> StandardMaterial3D: return Materials.flat(color, role) func _build_guard_spawn() -> void: var marker := Marker3D.new() marker.name = "GuardSpawn" # Guard spawns on the landing at the top of the steps, facing the street. # The landing is at y=4*height+0.1 where height=0.16, so y=0.76 # The guard's head offset is 1.6m, so spawn at y=0.76-1.6=-0.84 to place feet on ground # But we want the guard at eye-level with the sidewalk, so adjust to y=0.1 for feet # Then the head will be at y=1.7 (1.6 offset + 0.1 base) marker.position = Vector3(0, 0.1, -2.0) root.add_child(marker) func _lock_indicator_material(color: Color) -> StandardMaterial3D: var mat := StandardMaterial3D.new() mat.albedo_color = color mat.emission_enabled = true mat.emission = color mat.emission_energy_multiplier = 1.5 return mat ## Mirror the door lock state visually (green open, red locked). func set_door_locked(locked: bool) -> void: if door_indicator == null: return var color := Color(0.9, 0.15, 0.15) if locked else Color(0.1, 0.9, 0.3) if door_indicator.material_override is StandardMaterial3D: (door_indicator.material_override as StandardMaterial3D).albedo_color = color (door_indicator.material_override as StandardMaterial3D).emission = color