@tool extends Window const FUZZY_SEARCH = preload("res://addons/transition_configurator/fuzzy_search.gd") const TRANSITION_PIECE_SCENE = "res://TransitionPiece.tscn" var transition_piece: TransitionPiece = null # UI elements @onready var search_input: LineEdit @onready var room_list: ItemList @onready var room_filter_label: Label @onready var arrival_list: ItemList @onready var arrival_label: Label @onready var status_label: Label @onready var apply_button: Button # Data var all_rooms: Array[Dictionary] = [] var current_room_path: String = "" func _ready() -> void: title = "Configure Transition Exit" min_size = Vector2i(900, 600) var main_vbox = VBoxContainer.new() main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT) main_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL main_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL main_vbox.add_theme_constant_override("separation", 10) add_child(main_vbox) # Add margins var margin = MarginContainer.new() margin.add_theme_constant_override("margin_left", 20) margin.add_theme_constant_override("margin_right", 20) margin.add_theme_constant_override("margin_top", 20) margin.add_theme_constant_override("margin_bottom", 20) margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL margin.size_flags_vertical = Control.SIZE_EXPAND_FILL main_vbox.add_child(margin) var content_vbox = VBoxContainer.new() content_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL content_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL content_vbox.add_theme_constant_override("separation", 15) margin.add_child(content_vbox) # Title var title_label = Label.new() title_label.text = "Configure Exit for: %s" % (transition_piece.name if transition_piece else "Unknown") title_label.add_theme_font_size_override("font_size", 18) content_vbox.add_child(title_label) # Main content - HSplitContainer var split = HSplitContainer.new() split.size_flags_horizontal = Control.SIZE_EXPAND_FILL split.size_flags_vertical = Control.SIZE_EXPAND_FILL split.split_offset = 300 content_vbox.add_child(split) # Left side - Room selection var left_vbox = VBoxContainer.new() left_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL left_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL left_vbox.add_theme_constant_override("separation", 10) split.add_child(left_vbox) var search_label = Label.new() search_label.text = "Search Rooms:" left_vbox.add_child(search_label) search_input = LineEdit.new() search_input.placeholder_text = "Type to filter rooms..." search_input.text_changed.connect(_on_search_changed) left_vbox.add_child(search_input) room_filter_label = Label.new() room_filter_label.text = "Showing all rooms" left_vbox.add_child(room_filter_label) room_list = ItemList.new() room_list.size_flags_vertical = Control.SIZE_EXPAND_FILL room_list.item_selected.connect(_on_room_selected) left_vbox.add_child(room_list) # Right side - Arrival point selection var right_vbox = VBoxContainer.new() right_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL right_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL right_vbox.add_theme_constant_override("separation", 10) split.add_child(right_vbox) arrival_label = Label.new() arrival_label.text = "Select a room to see arrival points" arrival_label.add_theme_font_size_override("font_size", 14) right_vbox.add_child(arrival_label) arrival_list = ItemList.new() arrival_list.size_flags_vertical = Control.SIZE_EXPAND_FILL arrival_list.item_selected.connect(func(idx): _validate_selection()) right_vbox.add_child(arrival_list) # Status label status_label = Label.new() status_label.text = "" content_vbox.add_child(status_label) # Buttons var button_hbox = HBoxContainer.new() button_hbox.alignment = BoxContainer.ALIGNMENT_END button_hbox.add_theme_constant_override("separation", 10) content_vbox.add_child(button_hbox) apply_button = Button.new() apply_button.text = "Apply Changes" apply_button.disabled = true apply_button.custom_minimum_size = Vector2(150, 40) apply_button.pressed.connect(_on_apply) button_hbox.add_child(apply_button) var cancel_button = Button.new() cancel_button.text = "Cancel" cancel_button.custom_minimum_size = Vector2(100, 40) cancel_button.pressed.connect(hide) button_hbox.add_child(cancel_button) # Close on escape close_requested.connect(hide) func populate_rooms() -> void: all_rooms.clear() room_list.clear() var scene_files = _find_room_scenes() for file_path in scene_files: var room_name = file_path.get_file().get_basename() var uid = _get_scene_uid(file_path) all_rooms.append({ "name": room_name, "path": file_path, "uid": uid }) # Sort by name all_rooms.sort_custom(func(a, b): return a["name"] < b["name"]) _update_room_list(all_rooms) room_filter_label.text = "Showing %d rooms" % all_rooms.size() func _update_room_list(rooms: Array) -> void: room_list.clear() for room in rooms: room_list.add_item(room["name"]) room_list.set_item_metadata(room_list.item_count - 1, room) func _on_search_changed(new_text: String) -> void: if new_text.is_empty(): _update_room_list(all_rooms) room_filter_label.text = "Showing all %d rooms" % all_rooms.size() else: var filtered = FUZZY_SEARCH.sort_by_match(new_text, all_rooms, "name") _update_room_list(filtered) room_filter_label.text = "Showing %d of %d rooms" % [filtered.size(), all_rooms.size()] func _on_room_selected(index: int) -> void: arrival_list.clear() apply_button.disabled = true if index < 0 or index >= room_list.item_count: return var room_data = room_list.get_item_metadata(index) current_room_path = room_data["path"] arrival_label.text = "Arrival Points in %s:" % room_data["name"] # Find all TransitionPieces in the selected room var transitions = _find_transition_pieces_in_scene(current_room_path) if transitions.is_empty(): arrival_list.add_item("No TransitionPieces found in this room") arrival_list.set_item_disabled(0, true) status_label.text = "Warning: No arrival points available" else: for trans in transitions: arrival_list.add_item(trans["name"]) arrival_list.set_item_metadata(arrival_list.item_count - 1, trans) status_label.text = "" _validate_selection() func _validate_selection() -> void: var room_idx = room_list.get_selected_items() var arrival_idx = arrival_list.get_selected_items() if room_idx.is_empty(): apply_button.disabled = true status_label.text = "Please select a destination room" return if arrival_idx.is_empty(): apply_button.disabled = true status_label.text = "Please select an arrival point" return # Check if the selected arrival item is disabled (e.g., "No TransitionPieces found") if arrival_list.is_item_disabled(arrival_idx[0]): apply_button.disabled = true status_label.text = "Please select a valid arrival point" return var arrival_data = arrival_list.get_item_metadata(arrival_idx[0]) print("DEBUG: arrival_data = ", arrival_data, " type = ", typeof(arrival_data)) if arrival_data == null: apply_button.disabled = true status_label.text = "No arrival point data (null)" return if typeof(arrival_data) != TYPE_DICTIONARY: apply_button.disabled = true status_label.text = "Invalid arrival point data type: " + str(typeof(arrival_data)) return apply_button.disabled = false status_label.text = "" func _on_apply() -> void: var room_indices = room_list.get_selected_items() var arrival_indices = arrival_list.get_selected_items() if room_indices.is_empty() or arrival_indices.is_empty(): return var room_data = room_list.get_item_metadata(room_indices[0]) var arrival_data = arrival_list.get_item_metadata(arrival_indices[0]) var dest_path = room_data["path"] var dest_uid = room_data["uid"] var arrival_node_name = arrival_data["name"] if transition_piece == null: push_error("TransitionPiece is null") return # Update the source TransitionPiece var undo_redo = EditorInterface.get_editor_undo_redo() undo_redo.create_action("Configure Transition Exit") # Update target and appear_at_node on source undo_redo.add_undo_property(transition_piece, "target", transition_piece.target) undo_redo.add_undo_property(transition_piece, "appear_at_node", transition_piece.appear_at_node) undo_redo.add_do_property(transition_piece, "target", dest_uid) undo_redo.add_do_property(transition_piece, "appear_at_node", arrival_node_name) # Apply changes to source transition_piece.target = dest_uid transition_piece.appear_at_node = arrival_node_name transition_piece.notify_property_list_changed() # Try to update bidirectionally var bidirectional_result = _update_bidirectional_connection(dest_path, arrival_node_name) undo_redo.commit_action() if bidirectional_result.is_empty(): print("Transition configured successfully!") else: push_warning(bidirectional_result) hide() func _update_bidirectional_connection(dest_scene_path: String, arrival_node_name: String) -> String: """ Update the destination scene to create/update the return transition. Returns empty string on success, error message on failure. """ var current_scene = EditorInterface.get_edited_scene_root() if current_scene == null: return "Could not determine current scene" var current_scene_path = current_scene.scene_file_path var current_room_name = transition_piece.name # Load destination packed scene var dest_packed = load(dest_scene_path) as PackedScene if dest_packed == null: return "Failed to load destination scene: %s" % dest_scene_path var dest_state = dest_packed.get_state() var found_transition_idx = -1 var found_transition_node_name = "" # Find existing TransitionPiece that points back to current room for i in range(dest_state.get_node_count()): var node_instance = dest_state.get_node_instance(i) if node_instance == null: continue # Check if this is a TransitionPiece instance var instance_path = node_instance.resource_path if instance_path != TRANSITION_PIECE_SCENE: continue var node_name = dest_state.get_node_name(i) if node_name == current_room_name: found_transition_idx = i found_transition_node_name = node_name break # Reload scene for editing var dest_scene = dest_packed.instantiate() if dest_scene == null: return "Failed to instantiate destination scene" var return_transition: TransitionPiece = null if found_transition_idx >= 0: # Update existing transition return_transition = dest_scene.get_node_or_null(found_transition_node_name) if return_transition == null: return "Found transition index but couldn't get node: %s" % found_transition_node_name else: # Create new TransitionPiece return_transition = load(TRANSITION_PIECE_SCENE).instantiate() return_transition.name = current_room_name dest_scene.add_child(return_transition) return_transition.owner = dest_scene # Update the return transition properties var current_scene_uid = _get_scene_uid(current_scene_path) return_transition.target = current_scene_uid return_transition.appear_at_node = transition_piece.name # Save the modified scene var new_packed = PackedScene.new() var pack_result = new_packed.pack(dest_scene) if pack_result != OK: dest_scene.free() return "Failed to pack modified scene: %s" % error_string(pack_result) var save_result = ResourceSaver.save(new_packed, dest_scene_path) dest_scene.free() if save_result != OK: return "Failed to save destination scene: %s" % error_string(save_result) # Reload the scene in editor EditorInterface.reload_scene_from_path(dest_scene_path) return "" func _find_room_scenes(path: String = "res://scenes/") -> PackedStringArray: var results: PackedStringArray = [] var dir = DirAccess.open(path) if not dir: push_error("Could not open directory: " + path) return results dir.list_dir_begin() var file_name = dir.get_next() while file_name != "": var full_path = path.path_join(file_name) if dir.current_is_dir(): if not file_name.begins_with(".") and file_name != "addons": results.append_array(_find_room_scenes(full_path)) else: if file_name.ends_with(".tscn") and file_name.begins_with("kq4_"): results.append(full_path) file_name = dir.get_next() dir.list_dir_end() return results func _get_scene_uid(scene_path: String) -> String: """Get the UID for a scene file.""" var file = FileAccess.open(scene_path, FileAccess.READ) if not file: # Fallback to using the path return scene_path var first_line = file.get_line() file.close() # Parse uid="uid://xxxxx" from the header var regex = RegEx.new() regex.compile('uid="(uid://[^"]+)"') var result = regex.search(first_line) if result: return result.get_string(1) # Fallback: use ResourceUID var uid_path = ResourceUID.path_to_uid(scene_path) if uid_path.begins_with("uid://"): return uid_path return scene_path func _find_transition_pieces_in_scene(scene_path: String) -> Array[Dictionary]: """Find all TransitionPiece nodes in a scene file.""" var results: Array[Dictionary] = [] var packed = load(scene_path) as PackedScene if packed == null: return results var state = packed.get_state() for i in range(state.get_node_count()): var node_instance = state.get_node_instance(i) if node_instance == null: continue # Check if this is a TransitionPiece instance var instance_path = node_instance.resource_path if instance_path != TRANSITION_PIECE_SCENE: continue var node_name = state.get_node_name(i) results.append({ "name": node_name, "index": i }) return results