class_name InspectorPanel extends VBoxContainer ## Property and behavior editor for the selected object (docs/architecture/editor.md ## ยง7.2 `InspectorPanel`). ## ## Visual Property & Behavior Inspector (Creator Studio): ## - Position, yaw, scale, and delete operations. ## - Visual rule builder for ScriptBehavior rules: ## Triggers: on_interact, on_touch, on_enter, on_timer ## Actions: give_item (Buffalo Chips / items), rotate, move, play_sound, spawn_from_catalog ## - Pure UI: every property & behavior edit calls back into `EditorCore` ## (`move_object`, `rotate_object`, `scale_object`, `set_object_behavior`, `delete_object`), ## fully supporting undo and redo. signal delete_requested(object_id: String) var _core: EditorCore = null var _object_id: String = "" var _updating := false # guards against feedback loops while refreshing fields var _empty_label: Label var _scroll: ScrollContainer var _fields_box: VBoxContainer # Property fields var _id_label: Label var _type_label: Label var _asset_label: Label var _pos_x: SpinBox var _pos_y: SpinBox var _pos_z: SpinBox var _yaw: SpinBox var _scale_x: SpinBox var _scale_y: SpinBox var _scale_z: SpinBox var _delete_button: Button # Animation & Motion fields var _anim_clip_option: OptionButton var _anim_autoplay_check: CheckBox var _anim_loop_check: CheckBox var _anim_speed_spin: SpinBox var _motion_type_option: OptionButton var _motion_trigger_option: OptionButton var _motion_rotate_box: VBoxContainer var _motion_speed_spin: SpinBox var _motion_bob_box: VBoxContainer var _motion_bob_amp_spin: SpinBox var _motion_bob_freq_spin: SpinBox var _motion_waypoint_box: VBoxContainer var _motion_wp_speed_spin: SpinBox var _motion_wp_pause_spin: SpinBox var _motion_wp_count_label: Label var _motion_add_wp_btn: Button var _motion_clear_wp_btn: Button var _current_waypoints: Array[Vector3] = [] # Behavior & Rules fields var _rules_vbox: VBoxContainer var _add_rule_btn: Button var _rule_form_box: VBoxContainer var _trigger_option: OptionButton var _timer_row: HBoxContainer var _timer_interval_spin: SpinBox var _action_option: OptionButton # Dynamic Action Parameter boxes var _action_item_box: VBoxContainer var _item_id_edit: LineEdit var _item_count_spin: SpinBox var _action_rotate_box: VBoxContainer var _rotate_axis_option: OptionButton var _rotate_degrees_spin: SpinBox var _rotate_duration_spin: SpinBox var _action_move_box: VBoxContainer var _move_x_spin: SpinBox var _move_y_spin: SpinBox var _move_z_spin: SpinBox var _move_duration_spin: SpinBox var _action_sound_box: VBoxContainer var _sound_id_edit: LineEdit var _action_spawn_box: VBoxContainer var _spawn_catalog_id_edit: LineEdit var _spawn_x_spin: SpinBox var _spawn_y_spin: SpinBox var _spawn_z_spin: SpinBox var _is_built := false func _init() -> void: name = "InspectorPanel" size_flags_vertical = Control.SIZE_EXPAND_FILL func _ready() -> void: if not _is_built: _build_ui() if _object_id != "": show_object(_object_id) func _build_ui() -> void: if _is_built: return _is_built = true var header := Label.new() header.text = "Inspector" header.add_theme_font_size_override("font_size", 15) header.add_theme_color_override("font_color", Color(1, 0.95, 0.4)) add_child(header) _empty_label = Label.new() _empty_label.text = "(nothing selected)" add_child(_empty_label) _scroll = ScrollContainer.new() _scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL _scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED add_child(_scroll) _fields_box = VBoxContainer.new() _fields_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL _fields_box.visible = false _scroll.add_child(_fields_box) _id_label = _add_readonly_row("id") _type_label = _add_readonly_row("type") _asset_label = _add_readonly_row("asset") _pos_x = _add_spinbox_row("pos x") _pos_y = _add_spinbox_row("pos y") _pos_z = _add_spinbox_row("pos z (height)") _pos_x.value_changed.connect(_on_position_changed) _pos_y.value_changed.connect(_on_position_changed) _pos_z.value_changed.connect(_on_position_changed) _yaw = _add_spinbox_row("rotation (deg)") _yaw.min_value = -360.0 _yaw.max_value = 360.0 _yaw.step = 1.0 _yaw.value_changed.connect(_on_yaw_changed) _scale_x = _add_spinbox_row("scale x") _scale_y = _add_spinbox_row("scale y") _scale_z = _add_spinbox_row("scale z") _scale_x.value_changed.connect(_on_scale_changed) _scale_y.value_changed.connect(_on_scale_changed) _scale_z.value_changed.connect(_on_scale_changed) _delete_button = Button.new() _delete_button.text = "Delete Object" _delete_button.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4)) _delete_button.pressed.connect(_on_delete_pressed) _fields_box.add_child(_delete_button) _setup_animation_and_motion_ui() _setup_behavior_ui() func _setup_animation_and_motion_ui() -> void: var sep := HSeparator.new() sep.custom_minimum_size = Vector2(0, 16) _fields_box.add_child(sep) var header := Label.new() header.text = "Animation & Movement" header.add_theme_font_size_override("font_size", 14) header.add_theme_color_override("font_color", Color(0.4, 0.9, 0.5)) _fields_box.add_child(header) var desc := Label.new() desc.text = "Configure 3D clip playback and procedural motion." desc.add_theme_font_size_override("font_size", 11) desc.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) _fields_box.add_child(desc) # --- Animation Section --- var anim_panel := PanelContainer.new() var anim_vbox := VBoxContainer.new() anim_panel.add_child(anim_vbox) _fields_box.add_child(anim_panel) var anim_lbl := Label.new() anim_lbl.text = "Clip Animation" anim_lbl.add_theme_font_size_override("font_size", 12) anim_lbl.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4)) anim_vbox.add_child(anim_lbl) var clip_row := HBoxContainer.new() var clip_lbl := Label.new() clip_lbl.text = "Clip:" clip_lbl.custom_minimum_size = Vector2(60, 0) clip_row.add_child(clip_lbl) _anim_clip_option = OptionButton.new() _anim_clip_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL _anim_clip_option.item_selected.connect(_on_animation_changed) clip_row.add_child(_anim_clip_option) anim_vbox.add_child(clip_row) var anim_opts_row := HBoxContainer.new() _anim_autoplay_check = CheckBox.new() _anim_autoplay_check.text = "Autoplay" _anim_autoplay_check.toggled.connect(func(_val): _on_animation_changed(0)) anim_opts_row.add_child(_anim_autoplay_check) _anim_loop_check = CheckBox.new() _anim_loop_check.text = "Loop" _anim_loop_check.toggled.connect(func(_val): _on_animation_changed(0)) anim_opts_row.add_child(_anim_loop_check) anim_vbox.add_child(anim_opts_row) var speed_row := HBoxContainer.new() var spd_lbl := Label.new() spd_lbl.text = "Speed:" spd_lbl.custom_minimum_size = Vector2(60, 0) speed_row.add_child(spd_lbl) _anim_speed_spin = SpinBox.new() _anim_speed_spin.min_value = 0.1 _anim_speed_spin.max_value = 10.0 _anim_speed_spin.step = 0.1 _anim_speed_spin.value = 1.0 _anim_speed_spin.value_changed.connect(func(_val): _on_animation_changed(0)) speed_row.add_child(_anim_speed_spin) anim_vbox.add_child(speed_row) # --- Motion Section --- var motion_panel := PanelContainer.new() var motion_vbox := VBoxContainer.new() motion_panel.add_child(motion_vbox) _fields_box.add_child(motion_panel) var motion_lbl := Label.new() motion_lbl.text = "Procedural Motion" motion_lbl.add_theme_font_size_override("font_size", 12) motion_lbl.add_theme_color_override("font_color", Color(0.35, 0.85, 1.0)) motion_vbox.add_child(motion_lbl) var type_row := HBoxContainer.new() var type_lbl := Label.new() type_lbl.text = "Type:" type_lbl.custom_minimum_size = Vector2(60, 0) type_row.add_child(type_lbl) _motion_type_option = OptionButton.new() _motion_type_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL _motion_type_option.add_item("None (Static)", 0) _motion_type_option.set_item_metadata(0, "none") _motion_type_option.add_item("Rotate (Spin)", 1) _motion_type_option.set_item_metadata(1, "rotate") _motion_type_option.add_item("Bob / Float", 2) _motion_type_option.set_item_metadata(2, "bob") _motion_type_option.add_item("Waypoint Path", 3) _motion_type_option.set_item_metadata(3, "waypoint") _motion_type_option.item_selected.connect(_on_motion_type_selected) type_row.add_child(_motion_type_option) motion_vbox.add_child(type_row) var trig_row := HBoxContainer.new() var trig_lbl := Label.new() trig_lbl.text = "Trigger:" trig_lbl.custom_minimum_size = Vector2(60, 0) trig_row.add_child(trig_lbl) _motion_trigger_option = OptionButton.new() _motion_trigger_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL _motion_trigger_option.add_item("Always (Active)", 0) _motion_trigger_option.set_item_metadata(0, "always") _motion_trigger_option.add_item("Proximity (Player near)", 1) _motion_trigger_option.set_item_metadata(1, "proximity") _motion_trigger_option.add_item("Interact (Click)", 2) _motion_trigger_option.set_item_metadata(2, "interact") _motion_trigger_option.add_item("Signal (Script)", 3) _motion_trigger_option.set_item_metadata(3, "signal") _motion_trigger_option.item_selected.connect(func(_idx): _on_motion_param_changed()) trig_row.add_child(_motion_trigger_option) motion_vbox.add_child(trig_row) # Rotate parameters _motion_rotate_box = VBoxContainer.new() var rot_speed_row := HBoxContainer.new() var rot_spd_lbl := Label.new() rot_spd_lbl.text = "Speed (deg/s):" rot_spd_lbl.custom_minimum_size = Vector2(100, 0) rot_speed_row.add_child(rot_spd_lbl) _motion_speed_spin = SpinBox.new() _motion_speed_spin.min_value = -720.0 _motion_speed_spin.max_value = 720.0 _motion_speed_spin.value = 45.0 _motion_speed_spin.value_changed.connect(func(_v): _on_motion_param_changed()) rot_speed_row.add_child(_motion_speed_spin) _motion_rotate_box.add_child(rot_speed_row) motion_vbox.add_child(_motion_rotate_box) # Bob parameters _motion_bob_box = VBoxContainer.new() var bob_amp_row := HBoxContainer.new() var bob_amp_lbl := Label.new() bob_amp_lbl.text = "Amplitude:" bob_amp_lbl.custom_minimum_size = Vector2(80, 0) bob_amp_row.add_child(bob_amp_lbl) _motion_bob_amp_spin = SpinBox.new() _motion_bob_amp_spin.min_value = 0.05 _motion_bob_amp_spin.max_value = 20.0 _motion_bob_amp_spin.step = 0.1 _motion_bob_amp_spin.value = 0.5 _motion_bob_amp_spin.value_changed.connect(func(_v): _on_motion_param_changed()) bob_amp_row.add_child(_motion_bob_amp_spin) _motion_bob_box.add_child(bob_amp_row) var bob_freq_row := HBoxContainer.new() var bob_freq_lbl := Label.new() bob_freq_lbl.text = "Frequency:" bob_freq_lbl.custom_minimum_size = Vector2(80, 0) bob_freq_row.add_child(bob_freq_lbl) _motion_bob_freq_spin = SpinBox.new() _motion_bob_freq_spin.min_value = 0.1 _motion_bob_freq_spin.max_value = 10.0 _motion_bob_freq_spin.step = 0.1 _motion_bob_freq_spin.value = 1.0 _motion_bob_freq_spin.value_changed.connect(func(_v): _on_motion_param_changed()) bob_freq_row.add_child(_motion_bob_freq_spin) _motion_bob_box.add_child(bob_freq_row) motion_vbox.add_child(_motion_bob_box) # Waypoint parameters _motion_waypoint_box = VBoxContainer.new() var wp_spd_row := HBoxContainer.new() var wp_spd_lbl := Label.new() wp_spd_lbl.text = "Move Speed:" wp_spd_lbl.custom_minimum_size = Vector2(80, 0) wp_spd_row.add_child(wp_spd_lbl) _motion_wp_speed_spin = SpinBox.new() _motion_wp_speed_spin.min_value = 0.1 _motion_wp_speed_spin.max_value = 50.0 _motion_wp_speed_spin.value = 2.0 _motion_wp_speed_spin.value_changed.connect(func(_v): _on_motion_param_changed()) wp_spd_row.add_child(_motion_wp_speed_spin) _motion_waypoint_box.add_child(wp_spd_row) var wp_pause_row := HBoxContainer.new() var wp_pause_lbl := Label.new() wp_pause_lbl.text = "Pause (s):" wp_pause_lbl.custom_minimum_size = Vector2(80, 0) wp_pause_row.add_child(wp_pause_lbl) _motion_wp_pause_spin = SpinBox.new() _motion_wp_pause_spin.min_value = 0.0 _motion_wp_pause_spin.max_value = 60.0 _motion_wp_pause_spin.value = 0.0 _motion_wp_pause_spin.value_changed.connect(func(_v): _on_motion_param_changed()) wp_pause_row.add_child(_motion_wp_pause_spin) _motion_waypoint_box.add_child(wp_pause_row) _motion_wp_count_label = Label.new() _motion_wp_count_label.text = "Waypoints: 0" _motion_waypoint_box.add_child(_motion_wp_count_label) var wp_btn_row := HBoxContainer.new() _motion_add_wp_btn = Button.new() _motion_add_wp_btn.text = "+ Add Current Pos" _motion_add_wp_btn.pressed.connect(_on_add_current_pos_waypoint) wp_btn_row.add_child(_motion_add_wp_btn) _motion_clear_wp_btn = Button.new() _motion_clear_wp_btn.text = "Clear" _motion_clear_wp_btn.pressed.connect(_on_clear_waypoints) wp_btn_row.add_child(_motion_clear_wp_btn) _motion_waypoint_box.add_child(wp_btn_row) motion_vbox.add_child(_motion_waypoint_box) func _setup_behavior_ui() -> void: var sep := HSeparator.new() sep.custom_minimum_size = Vector2(0, 16) _fields_box.add_child(sep) var b_header := Label.new() b_header.text = "Behavior & Rules" b_header.add_theme_font_size_override("font_size", 14) b_header.add_theme_color_override("font_color", Color(0.35, 0.85, 1.0)) _fields_box.add_child(b_header) var b_desc := Label.new() b_desc.text = "Attach interactive logic and triggers." b_desc.add_theme_font_size_override("font_size", 11) b_desc.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) _fields_box.add_child(b_desc) _rules_vbox = VBoxContainer.new() _rules_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL _fields_box.add_child(_rules_vbox) _add_rule_btn = Button.new() _add_rule_btn.text = "+ Add Behavior Rule" _add_rule_btn.pressed.connect(_on_add_rule_btn_pressed) _fields_box.add_child(_add_rule_btn) _setup_rule_form() func _setup_rule_form() -> void: _rule_form_box = VBoxContainer.new() _rule_form_box.visible = false _rule_form_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL _fields_box.add_child(_rule_form_box) var form_panel := PanelContainer.new() var form_box := VBoxContainer.new() form_panel.add_child(form_box) _rule_form_box.add_child(form_panel) var form_title := Label.new() form_title.text = "New Behavior Rule" form_title.add_theme_font_size_override("font_size", 13) form_title.add_theme_color_override("font_color", Color(1.0, 0.9, 0.3)) form_box.add_child(form_title) # Trigger Selector var trig_row := HBoxContainer.new() var trig_lbl := Label.new() trig_lbl.text = "Trigger:" trig_lbl.custom_minimum_size = Vector2(70, 0) trig_row.add_child(trig_lbl) _trigger_option = OptionButton.new() _trigger_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL _trigger_option.add_item("on_interact (Click/Interact)", 0) _trigger_option.set_item_metadata(0, "on_interact") _trigger_option.add_item("on_touch (Physical contact)", 1) _trigger_option.set_item_metadata(1, "on_touch") _trigger_option.add_item("on_enter (Enters zone)", 2) _trigger_option.set_item_metadata(2, "on_enter") _trigger_option.add_item("on_timer (Repeats every X sec)", 3) _trigger_option.set_item_metadata(3, "on_timer") _trigger_option.item_selected.connect(_on_trigger_changed) trig_row.add_child(_trigger_option) form_box.add_child(trig_row) # Timer interval row (only visible when on_timer is selected) _timer_row = HBoxContainer.new() _timer_row.visible = false var timer_lbl := Label.new() timer_lbl.text = "Interval (s):" timer_lbl.custom_minimum_size = Vector2(70, 0) _timer_row.add_child(timer_lbl) _timer_interval_spin = SpinBox.new() _timer_interval_spin.min_value = 0.1 _timer_interval_spin.max_value = 3600.0 _timer_interval_spin.step = 0.5 _timer_interval_spin.value = 2.0 _timer_row.add_child(_timer_interval_spin) form_box.add_child(_timer_row) # Action Selector var act_row := HBoxContainer.new() var act_lbl := Label.new() act_lbl.text = "Action:" act_lbl.custom_minimum_size = Vector2(70, 0) act_row.add_child(act_lbl) _action_option = OptionButton.new() _action_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL _action_option.add_item("give_item (Reward Chips/Items)", 0) _action_option.set_item_metadata(0, "give_item") _action_option.add_item("rotate (Spin Object)", 1) _action_option.set_item_metadata(1, "rotate") _action_option.add_item("move (Move / Elevate)", 2) _action_option.set_item_metadata(2, "move") _action_option.add_item("play_sound (Audio FX)", 3) _action_option.set_item_metadata(3, "play_sound") _action_option.add_item("spawn_from_catalog (Spawn)", 4) _action_option.set_item_metadata(4, "spawn_from_catalog") _action_option.item_selected.connect(_on_action_changed) act_row.add_child(_action_option) form_box.add_child(act_row) # Dynamic Parameter Boxes # 1. give_item _action_item_box = VBoxContainer.new() var item_id_row := HBoxContainer.new() var item_id_lbl := Label.new() item_id_lbl.text = "Item ID:" item_id_lbl.custom_minimum_size = Vector2(70, 0) item_id_row.add_child(item_id_lbl) _item_id_edit = LineEdit.new() _item_id_edit.text = "buffalo_chips" _item_id_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL item_id_row.add_child(_item_id_edit) _action_item_box.add_child(item_id_row) var item_cnt_row := HBoxContainer.new() var item_cnt_lbl := Label.new() item_cnt_lbl.text = "Amount:" item_cnt_lbl.custom_minimum_size = Vector2(70, 0) item_cnt_row.add_child(item_cnt_lbl) _item_count_spin = SpinBox.new() _item_count_spin.min_value = 1 _item_count_spin.max_value = 10000 _item_count_spin.value = 5 item_cnt_row.add_child(_item_count_spin) _action_item_box.add_child(item_cnt_row) form_box.add_child(_action_item_box) # 2. rotate _action_rotate_box = VBoxContainer.new() _action_rotate_box.visible = false var rot_axis_row := HBoxContainer.new() var rot_axis_lbl := Label.new() rot_axis_lbl.text = "Axis:" rot_axis_lbl.custom_minimum_size = Vector2(70, 0) rot_axis_row.add_child(rot_axis_lbl) _rotate_axis_option = OptionButton.new() _rotate_axis_option.add_item("y (Yaw)") _rotate_axis_option.set_item_metadata(0, "y") _rotate_axis_option.add_item("x (Pitch)") _rotate_axis_option.set_item_metadata(1, "x") _rotate_axis_option.add_item("z (Roll)") _rotate_axis_option.set_item_metadata(2, "z") rot_axis_row.add_child(_rotate_axis_option) _action_rotate_box.add_child(rot_axis_row) var rot_deg_row := HBoxContainer.new() var rot_deg_lbl := Label.new() rot_deg_lbl.text = "Degrees:" rot_deg_lbl.custom_minimum_size = Vector2(70, 0) rot_deg_row.add_child(rot_deg_lbl) _rotate_degrees_spin = SpinBox.new() _rotate_degrees_spin.min_value = -360.0 _rotate_degrees_spin.max_value = 360.0 _rotate_degrees_spin.step = 15.0 _rotate_degrees_spin.value = 90.0 rot_deg_row.add_child(_rotate_degrees_spin) _action_rotate_box.add_child(rot_deg_row) var rot_dur_row := HBoxContainer.new() var rot_dur_lbl := Label.new() rot_dur_lbl.text = "Duration (s):" rot_dur_lbl.custom_minimum_size = Vector2(70, 0) rot_dur_row.add_child(rot_dur_lbl) _rotate_duration_spin = SpinBox.new() _rotate_duration_spin.min_value = 0.05 _rotate_duration_spin.max_value = 5.0 _rotate_duration_spin.step = 0.1 _rotate_duration_spin.value = 0.4 rot_dur_row.add_child(_rotate_duration_spin) _action_rotate_box.add_child(rot_dur_row) form_box.add_child(_action_rotate_box) # 3. move _action_move_box = VBoxContainer.new() _action_move_box.visible = false var move_lbl := Label.new() move_lbl.text = "Offset (X, Y, Z):" _action_move_box.add_child(move_lbl) var move_coords := HBoxContainer.new() _move_x_spin = SpinBox.new() _move_x_spin.min_value = -100.0 _move_x_spin.max_value = 100.0 _move_x_spin.value = 0.0 move_coords.add_child(_move_x_spin) _move_y_spin = SpinBox.new() _move_y_spin.min_value = -100.0 _move_y_spin.max_value = 100.0 _move_y_spin.value = 2.0 move_coords.add_child(_move_y_spin) _move_z_spin = SpinBox.new() _move_z_spin.min_value = -100.0 _move_z_spin.max_value = 100.0 _move_z_spin.value = 0.0 move_coords.add_child(_move_z_spin) _action_move_box.add_child(move_coords) var move_dur_row := HBoxContainer.new() var move_dur_lbl := Label.new() move_dur_lbl.text = "Duration (s):" move_dur_lbl.custom_minimum_size = Vector2(70, 0) move_dur_row.add_child(move_dur_lbl) _move_duration_spin = SpinBox.new() _move_duration_spin.min_value = 0.05 _move_duration_spin.max_value = 5.0 _move_duration_spin.step = 0.1 _move_duration_spin.value = 0.4 move_dur_row.add_child(_move_duration_spin) _action_move_box.add_child(move_dur_row) form_box.add_child(_action_move_box) # 4. play_sound _action_sound_box = VBoxContainer.new() _action_sound_box.visible = false var snd_preset_row := HBoxContainer.new() var snd_preset_lbl := Label.new() snd_preset_lbl.text = "Preset:" snd_preset_lbl.custom_minimum_size = Vector2(70, 0) snd_preset_row.add_child(snd_preset_lbl) var snd_option := OptionButton.new() snd_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL snd_option.add_item("๐Ÿ”” Bell Chime (bell)", 0) snd_option.set_item_metadata(0, "bell") snd_option.add_item("๐Ÿช™ Coin Pickup (coin)", 1) snd_option.set_item_metadata(1, "coin") snd_option.add_item("๐Ÿ”˜ Button Click (click)", 2) snd_option.set_item_metadata(2, "click") snd_option.add_item("โœจ Magic Chime (chime)", 3) snd_option.set_item_metadata(3, "chime") snd_option.add_item("๐ŸŽบ Victory Fanfare (fanfare)", 4) snd_option.set_item_metadata(4, "fanfare") snd_option.add_item("๐Ÿšช Door Open (door_open)", 5) snd_option.set_item_metadata(5, "door_open") snd_preset_row.add_child(snd_option) var preview_btn := Button.new() preview_btn.text = "โ–ถ Preview" preview_btn.tooltip_text = "Listen to this sound effect" preview_btn.pressed.connect(func(): var snd_id := _sound_id_edit.text.strip_edges() var p := "res://assets/audio/%s.wav" % snd_id if ResourceLoader.exists(p): var st = load(p) if st is AudioStream: var asp := AudioStreamPlayer.new() asp.stream = st add_child(asp) asp.finished.connect(asp.queue_free) asp.play() ) snd_preset_row.add_child(preview_btn) _action_sound_box.add_child(snd_preset_row) var snd_row := HBoxContainer.new() var snd_lbl := Label.new() snd_lbl.text = "Sound ID:" snd_lbl.custom_minimum_size = Vector2(70, 0) snd_row.add_child(snd_lbl) _sound_id_edit = LineEdit.new() _sound_id_edit.text = "bell" _sound_id_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL snd_row.add_child(_sound_id_edit) _action_sound_box.add_child(snd_row) snd_option.item_selected.connect(func(idx: int): var meta = snd_option.get_item_metadata(idx) if meta != null: _sound_id_edit.text = str(meta) ) form_box.add_child(_action_sound_box) # 5. spawn_from_catalog _action_spawn_box = VBoxContainer.new() _action_spawn_box.visible = false var sp_row := HBoxContainer.new() var sp_lbl := Label.new() sp_lbl.text = "Catalog ID:" sp_lbl.custom_minimum_size = Vector2(70, 0) sp_row.add_child(sp_lbl) _spawn_catalog_id_edit = LineEdit.new() _spawn_catalog_id_edit.text = "cube" _spawn_catalog_id_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL sp_row.add_child(_spawn_catalog_id_edit) _action_spawn_box.add_child(sp_row) var sp_pos_lbl := Label.new() sp_pos_lbl.text = "Spawn Pos (X, Y, Z):" _action_spawn_box.add_child(sp_pos_lbl) var sp_coords := HBoxContainer.new() _spawn_x_spin = SpinBox.new() _spawn_x_spin.min_value = -100.0 _spawn_x_spin.max_value = 100.0 _spawn_x_spin.value = 0.0 sp_coords.add_child(_spawn_x_spin) _spawn_y_spin = SpinBox.new() _spawn_y_spin.min_value = -100.0 _spawn_y_spin.max_value = 100.0 _spawn_y_spin.value = 1.0 sp_coords.add_child(_spawn_y_spin) _spawn_z_spin = SpinBox.new() _spawn_z_spin.min_value = -100.0 _spawn_z_spin.max_value = 100.0 _spawn_z_spin.value = 0.0 sp_coords.add_child(_spawn_z_spin) _action_spawn_box.add_child(sp_coords) form_box.add_child(_action_spawn_box) # Form Buttons var btn_row := HBoxContainer.new() var save_btn := Button.new() save_btn.text = "Save Rule" save_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL save_btn.pressed.connect(_on_save_rule_pressed) btn_row.add_child(save_btn) var cancel_btn := Button.new() cancel_btn.text = "Cancel" cancel_btn.pressed.connect(_on_cancel_rule_pressed) btn_row.add_child(cancel_btn) form_box.add_child(btn_row) func setup(core: EditorCore) -> void: _core = core func show_object(object_id: String) -> void: _object_id = object_id if not _is_built: _build_ui() if object_id == "" or _core == null: if _empty_label != null: _empty_label.visible = true if _fields_box != null: _fields_box.visible = false return var obj := _core.model.find_object(object_id) if obj == null: if _empty_label != null: _empty_label.visible = true if _fields_box != null: _fields_box.visible = false return if _empty_label != null: _empty_label.visible = false if _fields_box != null: _fields_box.visible = true if _rule_form_box != null: _rule_form_box.visible = false if _add_rule_btn != null: _add_rule_btn.visible = true _updating = true _id_label.text = "id: %s" % obj.id _type_label.text = "type: %s" % obj.type _asset_label.text = "asset: %s" % obj.asset_id _pos_x.value = obj.position.x _pos_y.value = obj.position.y _pos_z.value = obj.position.z _yaw.value = rad_to_deg(Quaternion(obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w).get_euler().y) _scale_x.value = obj.scale.x _scale_y.value = obj.scale.y _scale_z.value = obj.scale.z _updating = false _refresh_animation_and_motion() _refresh_rules() func _refresh_animation_and_motion() -> void: if _core == null or _object_id == "": return var obj := _core.model.find_object(_object_id) if obj == null: return _updating = true # Refresh Animation _anim_clip_option.clear() _anim_clip_option.add_item("(None)", 0) _anim_clip_option.set_item_metadata(0, "") var avail: Array = obj.properties.get("available_animations", []) var anim_cfg: Dictionary = obj.properties.get("animation", {}) var current_clip: String = str(anim_cfg.get("clip", obj.properties.get("active_animation", ""))) var selected_idx := 0 for i in range(avail.size()): var clip_name := str(avail[i]) _anim_clip_option.add_item(clip_name, i + 1) _anim_clip_option.set_item_metadata(i + 1, clip_name) if clip_name == current_clip: selected_idx = i + 1 _anim_clip_option.selected = selected_idx _anim_autoplay_check.button_pressed = bool(anim_cfg.get("autoplay", true)) _anim_loop_check.button_pressed = bool(anim_cfg.get("loop", true)) _anim_speed_spin.value = float(anim_cfg.get("speed", 1.0)) # Refresh Motion var motion_cfg: Dictionary = obj.properties.get("motion", {}) var m_type := str(motion_cfg.get("motion_type", "none")).to_lower() match m_type: "rotate": _motion_type_option.selected = 1 "bob", "oscillate": _motion_type_option.selected = 2 "waypoint", "patrol": _motion_type_option.selected = 3 _: _motion_type_option.selected = 0 var m_trig := str(motion_cfg.get("trigger_mode", "always")).to_lower() match m_trig: "proximity": _motion_trigger_option.selected = 1 "interact": _motion_trigger_option.selected = 2 "signal": _motion_trigger_option.selected = 3 _: _motion_trigger_option.selected = 0 _motion_speed_spin.value = float(motion_cfg.get("rotation_speed", 45.0)) _motion_bob_amp_spin.value = float(motion_cfg.get("bob_amplitude", 0.5)) _motion_bob_freq_spin.value = float(motion_cfg.get("bob_frequency", 1.0)) _motion_wp_speed_spin.value = float(motion_cfg.get("move_speed", 2.0)) _motion_wp_pause_spin.value = float(motion_cfg.get("pause_time", 0.0)) _current_waypoints.clear() var raw_wps = motion_cfg.get("waypoints", []) if raw_wps is Array: for wp in raw_wps: if wp is Array and wp.size() >= 3: _current_waypoints.append(Vector3(wp[0], wp[1], wp[2])) elif wp is Vector3: _current_waypoints.append(wp) _motion_wp_count_label.text = "Waypoints: %d" % _current_waypoints.size() _on_motion_type_selected(_motion_type_option.selected) _updating = false func _on_animation_changed(_idx: int) -> void: if _updating or _core == null or _object_id == "": return var clip_meta = _anim_clip_option.get_item_metadata(_anim_clip_option.selected) var clip_name := str(clip_meta) if clip_meta != null else "" var anim_dict := { "clip": clip_name, "autoplay": _anim_autoplay_check.button_pressed, "loop": _anim_loop_check.button_pressed, "speed": _anim_speed_spin.value } _core.set_object_property(_object_id, "animation", anim_dict) func _on_motion_type_selected(idx: int) -> void: var m_type = _motion_type_option.get_item_metadata(idx) if m_type == null: m_type = "none" _motion_rotate_box.visible = (m_type == "rotate") _motion_bob_box.visible = (m_type == "bob") _motion_waypoint_box.visible = (m_type == "waypoint") if not _updating: _on_motion_param_changed() func _on_motion_param_changed() -> void: if _updating or _core == null or _object_id == "": return var type_meta = _motion_type_option.get_item_metadata(_motion_type_option.selected) var trig_meta = _motion_trigger_option.get_item_metadata(_motion_trigger_option.selected) var m_type := str(type_meta) if type_meta != null else "none" var m_trig := str(trig_meta) if trig_meta != null else "always" var wps_ser: Array = [] for wp in _current_waypoints: wps_ser.append([wp.x, wp.y, wp.z]) var motion_dict := { "motion_type": m_type, "trigger_mode": m_trig, "rotation_speed": _motion_speed_spin.value, "bob_amplitude": _motion_bob_amp_spin.value, "bob_frequency": _motion_bob_freq_spin.value, "move_speed": _motion_wp_speed_spin.value, "pause_time": _motion_wp_pause_spin.value, "waypoints": wps_ser } _core.set_object_property(_object_id, "motion", motion_dict) func _on_add_current_pos_waypoint() -> void: if _core == null or _object_id == "": return var obj := _core.model.find_object(_object_id) if obj != null: _current_waypoints.append(obj.position) _motion_wp_count_label.text = "Waypoints: %d" % _current_waypoints.size() _on_motion_param_changed() func _on_clear_waypoints() -> void: _current_waypoints.clear() _motion_wp_count_label.text = "Waypoints: 0" _on_motion_param_changed() func _refresh_rules() -> void: for child in _rules_vbox.get_children(): child.queue_free() if _core == null or _object_id == "": return var obj := _core.model.find_object(_object_id) if obj == null: return var b_dict: Variant = obj.properties.get("script_behavior", obj.properties.get("behavior")) var rules_arr: Array = [] if b_dict is Dictionary and b_dict.has("rules") and b_dict["rules"] is Array: rules_arr = b_dict["rules"] if rules_arr.is_empty(): var empty_lbl := Label.new() empty_lbl.text = "(No behavior rules attached)" empty_lbl.add_theme_font_size_override("font_size", 11) empty_lbl.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) _rules_vbox.add_child(empty_lbl) return for i in range(rules_arr.size()): var rule: Variant = rules_arr[i] if not (rule is Dictionary): continue var card := _create_rule_card(i, rule as Dictionary) _rules_vbox.add_child(card) func _create_rule_card(rule_index: int, rule: Dictionary) -> Control: var panel := PanelContainer.new() var row := HBoxContainer.new() row.size_flags_horizontal = Control.SIZE_EXPAND_FILL panel.add_child(row) var text_box := VBoxContainer.new() text_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(text_box) var trigger_dict: Dictionary = rule.get("trigger", {}) var trig_type: String = str(trigger_dict.get("type", "unknown")) var trig_label := Label.new() if trig_type == "on_timer": trig_label.text = "โฑ๏ธ on_timer (every %ss)" % str(trigger_dict.get("interval", 1.0)) elif trig_type == "on_touch": trig_label.text = "๐Ÿ’ฅ on_touch" elif trig_type == "on_enter": trig_label.text = "๐Ÿšถ on_enter" else: trig_label.text = "โšก on_interact" trig_label.add_theme_font_size_override("font_size", 12) trig_label.add_theme_color_override("font_color", Color(0.4, 0.9, 1.0)) text_box.add_child(trig_label) var actions_arr: Array = rule.get("actions", []) for act in actions_arr: if not (act is Dictionary): continue var act_dict: Dictionary = act var verb: String = str(act_dict.get("verb", "")) var act_lbl := Label.new() act_lbl.add_theme_font_size_override("font_size", 11) match verb: "give_item": var item_name: String = str(act_dict.get("item_id", "buffalo_chips")) if item_name == "buffalo_chips": item_name = "Buffalo Chips" act_lbl.text = "โ†’ Give %d %s" % [int(act_dict.get("count", 1)), item_name] "rotate": act_lbl.text = "โ†’ Rotate %.0fยฐ (%s axis)" % [float(act_dict.get("degrees", 90)), str(act_dict.get("axis", "y"))] "move": var off: Array = act_dict.get("offset", [0, 0, 0]) act_lbl.text = "โ†’ Move offset %s" % str(off) "play_sound": act_lbl.text = "โ†’ Play sound '%s'" % str(act_dict.get("sound_id", "")) "spawn_from_catalog": act_lbl.text = "โ†’ Spawn '%s'" % str(act_dict.get("catalog_id", "")) _: act_lbl.text = "โ†’ %s" % verb text_box.add_child(act_lbl) var del_btn := Button.new() del_btn.text = "โœ•" del_btn.tooltip_text = "Delete this rule" del_btn.pressed.connect(_on_delete_rule_pressed.bind(rule_index)) row.add_child(del_btn) return panel func _on_add_rule_btn_pressed() -> void: _rule_form_box.visible = true _add_rule_btn.visible = false func _on_cancel_rule_pressed() -> void: _rule_form_box.visible = false _add_rule_btn.visible = true func _on_trigger_changed(index: int) -> void: var trig_key: String = _trigger_option.get_item_metadata(index) _timer_row.visible = (trig_key == "on_timer") func _on_action_changed(index: int) -> void: var act_key: String = _action_option.get_item_metadata(index) _action_item_box.visible = (act_key == "give_item") _action_rotate_box.visible = (act_key == "rotate") _action_move_box.visible = (act_key == "move") _action_sound_box.visible = (act_key == "play_sound") _action_spawn_box.visible = (act_key == "spawn_from_catalog") func _on_save_rule_pressed() -> void: if _core == null or _object_id == "": return var obj := _core.model.find_object(_object_id) if obj == null: return var trig_type: String = _trigger_option.get_item_metadata(_trigger_option.selected) var trig_dict: Dictionary = {"type": trig_type} if trig_type == "on_timer": trig_dict["interval"] = _timer_interval_spin.value var act_verb: String = _action_option.get_item_metadata(_action_option.selected) var act_dict: Dictionary = {"verb": act_verb} match act_verb: "give_item": var item_id := _item_id_edit.text.strip_edges() if item_id.is_empty(): item_id = "buffalo_chips" act_dict["item_id"] = item_id act_dict["count"] = int(_item_count_spin.value) "rotate": var axis_idx := _rotate_axis_option.selected act_dict["axis"] = _rotate_axis_option.get_item_metadata(axis_idx) act_dict["degrees"] = _rotate_degrees_spin.value act_dict["duration"] = _rotate_duration_spin.value act_dict["target"] = "self" "move": act_dict["target"] = "self" act_dict["offset"] = [_move_x_spin.value, _move_y_spin.value, _move_z_spin.value] act_dict["duration"] = _move_duration_spin.value "play_sound": var snd := _sound_id_edit.text.strip_edges() if snd.is_empty(): snd = "bell" act_dict["sound_id"] = snd "spawn_from_catalog": var cat_id := _spawn_catalog_id_edit.text.strip_edges() if cat_id.is_empty(): cat_id = "cube" act_dict["catalog_id"] = cat_id act_dict["position"] = [_spawn_x_spin.value, _spawn_y_spin.value, _spawn_z_spin.value] var new_rule: Dictionary = { "trigger": trig_dict, "conditions": [], "actions": [act_dict] } var cur_behavior: Dictionary = {} var raw_b = obj.properties.get("script_behavior", obj.properties.get("behavior")) if raw_b is Dictionary: cur_behavior = (raw_b as Dictionary).duplicate(true) var cur_rules: Array = cur_behavior.get("rules", []).duplicate(true) cur_rules.append(new_rule) cur_behavior["rules"] = cur_rules _core.set_object_behavior(_object_id, cur_behavior) _rule_form_box.visible = false _add_rule_btn.visible = true _refresh_rules() func _on_delete_rule_pressed(index: int) -> void: if _core == null or _object_id == "": return var obj := _core.model.find_object(_object_id) if obj == null: return var raw_b = obj.properties.get("script_behavior", obj.properties.get("behavior")) if not (raw_b is Dictionary): return var cur_behavior: Dictionary = (raw_b as Dictionary).duplicate(true) var cur_rules: Array = cur_behavior.get("rules", []).duplicate(true) if index >= 0 and index < cur_rules.size(): cur_rules.remove_at(index) cur_behavior["rules"] = cur_rules _core.set_object_behavior(_object_id, cur_behavior) _refresh_rules() func _on_position_changed(_v: float) -> void: if _updating or _core == null or _object_id == "": return _core.move_object(_object_id, Vector3(_pos_x.value, _pos_y.value, _pos_z.value)) func _on_yaw_changed(v: float) -> void: if _updating or _core == null or _object_id == "": return var quat := Quaternion(Vector3.UP, deg_to_rad(v)) _core.rotate_object(_object_id, Vector4(quat.x, quat.y, quat.z, quat.w)) func _on_scale_changed(_v: float) -> void: if _updating or _core == null or _object_id == "": return _core.scale_object(_object_id, Vector3(_scale_x.value, _scale_y.value, _scale_z.value)) func _on_delete_pressed() -> void: if _object_id == "": return var id := _object_id delete_requested.emit(id) func _add_readonly_row(label: String) -> Label: var l := Label.new() l.text = "%s: " % label _fields_box.add_child(l) return l func _add_spinbox_row(label_text: String) -> SpinBox: var row := HBoxContainer.new() var l := Label.new() l.text = label_text l.custom_minimum_size = Vector2(90, 0) row.add_child(l) var sb := SpinBox.new() sb.min_value = -100000.0 sb.max_value = 100000.0 sb.step = 0.1 sb.allow_greater = true sb.allow_lesser = true row.add_child(sb) _fields_box.add_child(row) return sb