Files
ai-game-2/addons/transition_configurator/config_dialog.gd

432 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(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