extends Node ## Runtime debug panel. Toggle with F1. ## Reads DP.get_all() to build controls — adding a param to DP is all that's needed to show it here. var _panel: Control var _status_label: Label var _prev_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_CAPTURED func _ready() -> void: _build_ui() DP.any_changed.connect(_on_dp_changed) func _build_ui() -> void: var canvas := CanvasLayer.new() canvas.layer = 128 add_child(canvas) _panel = PanelContainer.new() _panel.anchor_left = 1.0 _panel.anchor_right = 1.0 _panel.anchor_top = 0.0 _panel.anchor_bottom = 1.0 _panel.offset_left = -390.0 _panel.offset_right = 0.0 _panel.offset_top = 0.0 _panel.offset_bottom = 0.0 _panel.mouse_filter = Control.MOUSE_FILTER_STOP _panel.visible = false var bg := StyleBoxFlat.new() bg.bg_color = Color(0.08, 0.08, 0.10, 0.95) _panel.add_theme_stylebox_override("panel", bg) canvas.add_child(_panel) var root_vbox := VBoxContainer.new() root_vbox.size_flags_horizontal = Control.SIZE_FILL _panel.add_child(root_vbox) # ── Toolbar ─────────────────────────────────────────────────────────────── var toolbar_margin := MarginContainer.new() toolbar_margin.add_theme_constant_override("margin_left", 8) toolbar_margin.add_theme_constant_override("margin_right", 8) toolbar_margin.add_theme_constant_override("margin_top", 6) toolbar_margin.add_theme_constant_override("margin_bottom", 4) root_vbox.add_child(toolbar_margin) var toolbar := HBoxContainer.new() toolbar.add_theme_constant_override("separation", 6) toolbar_margin.add_child(toolbar) var title := Label.new() title.text = "DEBUG [F1]" title.size_flags_horizontal = Control.SIZE_EXPAND_FILL toolbar.add_child(title) _status_label = Label.new() _status_label.modulate = Color(0.4, 1.0, 0.5) toolbar.add_child(_status_label) var save_btn := Button.new() save_btn.text = "Save" save_btn.pressed.connect(_on_save) toolbar.add_child(save_btn) var reset_btn := Button.new() reset_btn.text = "Reset All" reset_btn.pressed.connect(DP.reset_all) toolbar.add_child(reset_btn) root_vbox.add_child(HSeparator.new()) # ── Scrollable content ──────────────────────────────────────────────────── var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.size_flags_horizontal = Control.SIZE_FILL root_vbox.add_child(scroll) var content_margin := MarginContainer.new() content_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL content_margin.add_theme_constant_override("margin_left", 8) content_margin.add_theme_constant_override("margin_right", 8) content_margin.add_theme_constant_override("margin_top", 4) content_margin.add_theme_constant_override("margin_bottom", 8) scroll.add_child(content_margin) var content := VBoxContainer.new() content.size_flags_horizontal = Control.SIZE_EXPAND_FILL content_margin.add_child(content) _build_params(content) func _build_params(content: VBoxContainer) -> void: # Group keys by section preserving registration order var sections: Dictionary = {} for key: String in DP.get_all(): var sec: String = DP.get_all()[key]["section"] if not sections.has(sec): sections[sec] = [] sections[sec].append(key) for section: String in sections: content.add_child(HSeparator.new()) var header := Label.new() header.text = section.to_upper() header.modulate = Color(0.75, 0.85, 1.0) content.add_child(header) for key: String in sections[section]: var meta: Dictionary = DP.get_all()[key] if meta["type"] == TYPE_FLOAT: _add_float_row(content, key, meta) elif meta["type"] == TYPE_BOOL: _add_bool_row(content, key, meta) func _add_float_row(parent: VBoxContainer, key: String, meta: Dictionary) -> void: var row := HBoxContainer.new() row.size_flags_horizontal = Control.SIZE_FILL parent.add_child(row) var lbl := Label.new() lbl.text = key.replace("_", " ") lbl.custom_minimum_size.x = 140 lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER row.add_child(lbl) var slider := HSlider.new() slider.min_value = meta["min"] slider.max_value = meta["max"] slider.step = meta["step"] slider.value = meta["value"] slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL slider.custom_minimum_size.x = 60 row.add_child(slider) var spin := SpinBox.new() spin.min_value = meta["min"] spin.max_value = meta["max"] spin.step = meta["step"] spin.value = meta["value"] spin.custom_minimum_size.x = 85 row.add_child(spin) # Slider → SpinBox + DP (use no_signal to avoid echo) slider.value_changed.connect(func(v: float) -> void: spin.set_value_no_signal(v) DP.set_value(key, v) ) # SpinBox → Slider + DP spin.value_changed.connect(func(v: float) -> void: slider.set_value_no_signal(v) DP.set_value(key, v) ) # External changes (reset, load) → sync both controls DP.any_changed.connect(func(changed_key: String, _val: Variant) -> void: if changed_key != key and changed_key != "__all__": return var v := DP.f(key) slider.set_value_no_signal(v) spin.set_value_no_signal(v) ) func _add_bool_row(parent: VBoxContainer, key: String, meta: Dictionary) -> void: var row := HBoxContainer.new() row.size_flags_horizontal = Control.SIZE_FILL parent.add_child(row) var lbl := Label.new() lbl.text = key.replace("_", " ") lbl.custom_minimum_size.x = 140 lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER row.add_child(lbl) var btn := Button.new() btn.toggle_mode = true btn.button_pressed = meta["value"] btn.text = "HIDDEN" if not meta["value"] else "SHOWN" _update_btn_style(btn, meta["value"]) row.add_child(btn) btn.toggled.connect(func(v: bool) -> void: btn.text = "HIDDEN" if not v else "SHOWN" _update_btn_style(btn, v) DP.set_value(key, v) ) DP.any_changed.connect(func(changed_key: String, _val: Variant) -> void: if changed_key != key and changed_key != "__all__": return var v_b: bool = DP.b(key) btn.set_pressed_no_signal(v_b) btn.text = "HIDDEN" if not v_b else "SHOWN" _update_btn_style(btn, v_b) ) func _update_btn_style(btn: Button, active: bool) -> void: if active: btn.add_theme_color_override("font_color", Color(0.4, 1.0, 0.5)) else: btn.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4)) func _on_save() -> void: DP.save() _status_label.text = "Saved!" get_tree().create_timer(1.5).timeout.connect(func() -> void: _status_label.text = "" ) func _on_dp_changed(key: String, _value: Variant) -> void: if key == "show_collisions" or key == "__all__": var active: bool = DP.b("show_collisions") get_tree().debug_collisions_hint = active # Force refresh of all collision shapes _refresh_collisions(get_tree().root, active) func _refresh_collisions(node: Node, active: bool) -> void: if node is CollisionShape3D or node is CollisionPolygon3D: node.visible = !node.visible # Trigger a notification node.visible = !node.visible for child in node.get_children(): _refresh_collisions(child, active) func _input(event: InputEvent) -> void: if _panel.visible and Input.is_key_pressed(KEY_CTRL) and event is InputEventMouseMotion: get_viewport().set_input_as_handled() func _unhandled_input(event: InputEvent) -> void: if not (event is InputEventKey and event.pressed and not event.echo): return if (event as InputEventKey).keycode != KEY_F1: return _panel.visible = not _panel.visible if _panel.visible: _prev_mouse_mode = Input.mouse_mode Input.mouse_mode = Input.MOUSE_MODE_VISIBLE else: Input.mouse_mode = _prev_mouse_mode get_viewport().set_input_as_handled()