- Fixed validation error by checking TYPE_DICTIONARY instead of is_empty() - Removed hardcoded colors, using Godot's built-in theme colors instead
420 lines
13 KiB
GDScript
420 lines
13 KiB
GDScript
@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(_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
|
|
|
|
var arrival_data = arrival_list.get_item_metadata(arrival_idx[0])
|
|
if arrival_data == null or typeof(arrival_data) != TYPE_DICTIONARY:
|
|
apply_button.disabled = true
|
|
status_label.text = "Invalid arrival point selected"
|
|
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
|