@tool class_name CutsceneGraphEdit extends GraphEdit # Main graph editor interface # Signals signal node_added(node) signal node_removed(node) signal connection_created(from_node, from_port, to_node, to_port) signal connection_removed(from_node, from_port, to_node, to_port) signal graph_changed() # Properties var node_counter: int = 0 # For generating unique node IDs var current_cutscene: CutsceneResource # The cutscene being edited # Preview properties var preview_manager: PreviewManager var preview_panel: PreviewPanel # Undo/Redo properties var undo_redo_manager: UndoRedoManager # Called when the node is ready func _ready() -> void: # Set up GraphEdit properties set_right_disconnects(true) set_show_grid(true) set_snapping_enabled(true) set_snapping_distance(20) # Connect to GraphEdit signals connect("connection_request", _on_connection_request) connect("disconnection_request", _on_disconnection_request) connect("node_selected", _on_node_selected) connect("node_unselected", _on_node_unselected) connect("popup_request", _on_popup_request) connect("connection_to_empty", _on_connection_to_empty) connect("connection_from_empty", _on_connection_from_empty) # Add mini-map _setup_minimap() # Set up mini-map func _setup_minimap() -> void: # Enable mini-map set_minimap_enabled(true) set_minimap_size(Vector2(200, 150)) #set_minimap_offset(Vector2(10, 10)) # Add a new node to the graph func add_node(node_type: String, position: Vector2) -> BaseGraphNode: var new_node: BaseGraphNode match node_type: "entry": new_node = preload("res://addons/cutscene_editor/editor/nodes/EntryNode.gd").new() "exit": new_node = preload("res://addons/cutscene_editor/editor/nodes/ExitNode.gd").new() "move": new_node = preload("res://addons/cutscene_editor/editor/nodes/MoveActionNode.gd").new() "turn": new_node = preload("res://addons/cutscene_editor/editor/nodes/TurnActionNode.gd").new() "dialogue": new_node = preload("res://addons/cutscene_editor/editor/nodes/DialogueActionNode.gd").new() "animation": new_node = preload("res://addons/cutscene_editor/editor/nodes/AnimationActionNode.gd").new() "wait": new_node = preload("res://addons/cutscene_editor/editor/nodes/WaitActionNode.gd").new() "parallel": new_node = preload("res://addons/cutscene_editor/editor/nodes/ParallelGroupNode.gd").new() _: return null # Set node position new_node.position_offset = position # Special handling for parallel groups if new_node is ParallelGroupNode: # Set a larger initial size for parallel groups new_node.custom_minimum_size = Vector2(200, 150) # Add to GraphEdit add_child(new_node) # Connect node signals new_node.connect("node_selected", _on_graph_node_selected) new_node.connect("node_deleted", _on_graph_node_deleted) new_node.connect("parameter_changed", _on_node_parameter_changed) # Emit signal emit_signal("node_added", new_node) emit_signal("graph_changed") return new_node # Handle connection requests func _on_connection_request(from_node: String, from_port: int, to_node: String, to_port: int) -> void: # Validate connection before creating it if _is_connection_valid(from_node, from_port, to_node, to_port): # Get node references var from_node_instance = get_node_or_null(from_node) var to_node_instance = get_node_or_null(to_node) # Special handling for parallel groups if to_node_instance is ParallelGroupNode: # Connecting to a parallel group if _is_valid_parallel_connection(from_node_instance, from_port, to_node_instance, to_port): # Add child node to parallel group if to_node_instance.can_add_child_node(from_node_instance): to_node_instance.add_child_node(from_node_instance) # Create a logical connection (not visual) _create_logical_parallel_connection(from_node, from_port, to_node, to_port) # Emit signal emit_signal("connection_created", from_node, from_port, to_node, to_port) emit_signal("graph_changed") return # Create the connection connect_node(from_node, from_port, to_node, to_port) # Emit signal emit_signal("connection_created", from_node, from_port, to_node, to_port) emit_signal("graph_changed") else: # Show error feedback _show_connection_error(from_node, from_port, to_node, to_port) # Handle disconnection requests func _on_disconnection_request(from_node: String, from_port: int, to_node: String, to_port: int) -> void: # Get node references var from_node_instance = get_node_or_null(from_node) var to_node_instance = get_node_or_null(to_node) # Special handling for parallel groups if to_node_instance is ParallelGroupNode: # Remove child node from parallel group if from_node_instance in to_node_instance.child_nodes: to_node_instance.remove_child_node(from_node_instance) # Remove logical connection _remove_logical_parallel_connection(from_node, from_port, to_node, to_port) # Emit signal emit_signal("connection_removed", from_node, from_port, to_node, to_port) emit_signal("graph_changed") return # Remove the connection disconnect_node(from_node, from_port, to_node, to_port) # Emit signal emit_signal("connection_removed", from_node, from_port, to_node, to_port) emit_signal("graph_changed") # Validate if a connection is valid func _is_connection_valid(from_node_name: String, from_port: int, to_node_name: String, to_port: int) -> bool: # Get node references var from_node = get_node_or_null(from_node_name) var to_node = get_node_or_null(to_node_name) if not from_node or not to_node: return false # Prevent connections to self if from_node == to_node: return false # Prevent duplicate connections if is_node_connected(from_node_name, from_port, to_node_name, to_port): return false # Prevent circular dependencies if _would_create_cycle(from_node_name, to_node_name): return false # Validate connection based on node types match from_node.node_type: "entry": # Entry can connect to any node except entry/exit return to_node.node_type != "entry" and to_node.node_type != "exit" "exit": # Exit cannot have outgoing connections return false "parallel": # Parallel group output can connect to any node except entry/parallel if from_port == from_node.input_connections: # Output port return to_node.node_type != "entry" and to_node.node_type != "parallel" else: # Input port return to_node.node_type != "exit" and to_node.node_type != "parallel" _: # Other nodes can connect to any node except entry return to_node.node_type != "entry" return true # Check if creating a connection would create a cycle func _would_create_cycle(from_node_name: String, to_node_name: String) -> bool: # Simple cycle detection using depth-first search var visited = {} var recursion_stack = {} # Start from the target node and see if we can reach the source node return _dfs_cycle_check(to_node_name, from_node_name, visited, recursion_stack) # Depth-first search for cycle detection func _dfs_cycle_check(current_node_name: String, target_node_name: String, visited: Dictionary, recursion_stack: Dictionary) -> bool: # Mark current node as visited and add to recursion stack visited[current_node_name] = true recursion_stack[current_node_name] = true # Check all outgoing connections for connection in get_connection_list(): if connection["from_node"] == current_node_name: var next_node_name = connection["to_node"] # If we reached the target node, we found a cycle if next_node_name == target_node_name: return true # If next node not visited, recursively check if not visited.has(next_node_name): if _dfs_cycle_check(next_node_name, target_node_name, visited, recursion_stack): return true # If next node is in recursion stack, we found a cycle elif recursion_stack.has(next_node_name): return true # Check parallel connections if has_meta("logical_connections"): var logical_connections = get_meta("logical_connections") for connection in logical_connections: if connection["from_node"] == current_node_name: var next_node_name = connection["to_node"] # If we reached the target node, we found a cycle if next_node_name == target_node_name: return true # If next node not visited, recursively check if not visited.has(next_node_name): if _dfs_cycle_check(next_node_name, target_node_name, visited, recursion_stack): return true # If next node is in recursion stack, we found a cycle elif recursion_stack.has(next_node_name): return true # Remove from recursion stack recursion_stack[current_node_name] = false return false # Show connection error feedback func _show_connection_error(from_node: String, from_port: int, to_node: String, to_port: int) -> void: # Visual feedback for invalid connection # This could be a temporary red highlight or a popup message print("Invalid connection: %s -> %s" % [from_node, to_node]) # Handle popup request (right-click context menu) func _on_popup_request(position: Vector2) -> void: # Create context menu for adding nodes var menu = PopupMenu.new() menu.name = "NodeContextMenu" # Add node types to menu menu.add_item("Add Entry Node", 0) menu.add_item("Add Exit Node", 1) menu.add_item("Add Move Action", 2) menu.add_item("Add Turn Action", 3) menu.add_item("Add Dialogue Action", 4) menu.add_item("Add Animation Action", 5) menu.add_item("Add Wait Action", 6) menu.add_item("Add Parallel Group", 7) # Connect menu signals menu.connect("id_pressed", _on_context_menu_selected.bind(position)) # Add menu to scene and show it add_child(menu) menu.position = position menu.popup() # Handle context menu selection func _on_context_menu_selected(id: int, position: Vector2) -> void: var node_type: String match id: 0: node_type = "entry" 1: node_type = "exit" 2: node_type = "move" 3: node_type = "turn" 4: node_type = "dialogue" 5: node_type = "animation" 6: node_type = "wait" 7: node_type = "parallel" _: return # Add the selected node type add_node(node_type, position) # Handle zoom in func _on_zoom_in_pressed() -> void: # Increase zoom level zoom *= 1.2 _update_zoom_label() # Handle zoom out func _on_zoom_out_pressed() -> void: # Decrease zoom level zoom /= 1.2 _update_zoom_label() # Update zoom label func _update_zoom_label() -> void: var zoom_label = get_node_or_null("ZoomControls/ZoomLabel") if zoom_label: zoom_label.text = "%d%%" % (zoom * 100) # Handle node selection func _on_node_selected(node_name: String) -> void: # Handle GraphEdit's built-in node selection pass # Handle node unselection func _on_node_unselected(node_name: String) -> void: # Handle GraphEdit's built-in node unselection pass # Handle graph node selection func _on_graph_node_selected(node: BaseGraphNode) -> void: # Handle custom node selection pass # Handle graph node deletion func _on_graph_node_deleted(node: BaseGraphNode) -> void: # Special handling for parallel groups if node is ParallelGroupNode: # Remove all child nodes first for child_node in node.child_nodes: # Remove child from scene tree if child_node.get_parent(): child_node.get_parent().remove_child(child_node) # Add child back to main graph add_child(child_node) # Update child position to be near the parallel group child_node.position_offset = node.position_offset + Vector2(50, 50) # Clear child nodes array node.child_nodes.clear() # Remove all connections to/from this node var connections = get_connection_list() for connection in connections: if connection["from_node"] == node.name or connection["to_node"] == node.name: disconnect_node(connection["from_node"], connection["from_port"], connection["to_node"], connection["to_port"]) # Remove logical connections for parallel groups if has_meta("logical_connections"): var logical_connections = get_meta("logical_connections") var i = 0 while i < logical_connections.size(): var conn = logical_connections[i] if conn["from_node"] == node.name or conn["to_node"] == node.name: logical_connections.remove_at(i) else: i += 1 # Remove node from GraphEdit remove_child(node) # Emit signals emit_signal("node_removed", node) emit_signal("graph_changed") # Handle node parameter changes func _on_node_parameter_changed(node: BaseGraphNode, parameter_name: String, new_value) -> void: # Handle parameter changes in nodes emit_signal("graph_changed") # Handle connection to empty space func _on_connection_to_empty(from_node: String, from_port: int, release_position: Vector2) -> void: # This could be used to show a menu or create a new node pass # Handle connection from empty space func _on_connection_from_empty(to_node: String, to_port: int, release_position: Vector2) -> void: # This could be used to show a menu or create a new node pass # Clear the graph func clear_graph() -> void: # Remove all nodes for child in get_children(): if child is BaseGraphNode: remove_child(child) child.queue_free() # Clear all connections clear_connections() # Reset node counter node_counter = 0 # Emit signal emit_signal("graph_changed") # Load graph from cutscene resource func load_from_cutscene(cutscene: CutsceneResource) -> void: # Clear existing graph clear_graph() # Set current cutscene current_cutscene = cutscene # Create nodes from cutscene data for node_data in cutscene.nodes: var node = add_node(node_data["type"], Vector2(node_data["x"], node_data["y"])) if node: node.name = node_data["name"] # Set node parameters for param_name in node_data["parameters"]: node.set_parameter(param_name, node_data["parameters"][param_name]) # Create connections from cutscene data for connection_data in cutscene.connections: connect_node(connection_data["from_node"], connection_data["from_port"], connection_data["to_node"], connection_data["to_port"]) # Emit signal emit_signal("graph_changed") # Save graph to cutscene resource func save_to_cutscene() -> CutsceneResource: if not current_cutscene: current_cutscene = preload("res://addons/cutscene_editor/editor/resources/CutsceneResource.gd").new() # Clear existing data current_cutscene.nodes.clear() current_cutscene.connections.clear() # Save nodes for child in get_children(): if child is BaseGraphNode: var node_data = { "name": child.name, "type": child.node_type, "x": child.position_offset.x, "y": child.position_offset.y, "parameters": child.action_parameters } current_cutscene.nodes.append(node_data) # Save connections for connection in get_connection_list(): var connection_data = { "from_node": connection["from_node"], "from_port": connection["from_port"], "to_node": connection["to_node"], "to_port": connection["to_port"] } current_cutscene.connections.append(connection_data) return current_cutscene # Special handling for parallel groups func _is_valid_parallel_connection(from_node: BaseGraphNode, from_port: int, to_node: ParallelGroupNode, to_port: int) -> bool: # Can only connect to input ports of parallel groups if to_port >= to_node.input_connections: return false # Can't connect parallel group to itself if from_node == to_node: return false # Can't connect entry or exit nodes to parallel groups if from_node.node_type == "entry" or from_node.node_type == "exit": return false # Can't connect parallel groups to parallel groups if from_node.node_type == "parallel": return false return true # Create a logical connection for parallel groups (not visually represented) func _create_logical_parallel_connection(from_node: String, from_port: int, to_node: String, to_port: int) -> void: # Store the logical connection in our data structure # This connection won't be visually represented but will be used for generation var logical_connection = { "from_node": from_node, "from_port": from_port, "to_node": to_node, "to_port": to_port, "is_parallel": true } # Add to a separate list of logical connections if not has_meta("logical_connections"): set_meta("logical_connections", []) var logical_connections = get_meta("logical_connections") logical_connections.append(logical_connection) # Remove a logical connection for parallel groups func _remove_logical_parallel_connection(from_node: String, from_port: int, to_node: String, to_port: int) -> void: if not has_meta("logical_connections"): return var logical_connections = get_meta("logical_connections") var i = 0 while i < logical_connections.size(): var conn = logical_connections[i] if (conn["from_node"] == from_node and conn["from_port"] == from_port and conn["to_node"] == to_node and conn["to_port"] == to_port): logical_connections.remove_at(i) return i += 1 # Get all logical connections func get_logical_connections() -> Array: if has_meta("logical_connections"): return get_meta("logical_connections") return [] # Set up preview system func setup_preview() -> void: # Create preview manager preview_manager = PreviewManager.new() add_child(preview_manager) # Create preview panel preview_panel = PreviewPanel.new() preview_panel.set_preview_manager(preview_manager) preview_panel.set_graph_edit(self) # Add to editor interface (this would be added to the editor UI) # For now, we'll just keep a reference to it # Start preview func start_preview() -> void: if preview_manager: preview_manager.start_preview() # Stop preview func stop_preview() -> void: if preview_manager: preview_manager.stop_preview() # Pause preview func pause_preview() -> void: if preview_manager: preview_manager.pause_preview() # Resume preview func resume_preview() -> void: if preview_manager: preview_manager.resume_preview() # Set up undo/redo system func _setup_undo_redo() -> void: # Create undo/redo manager undo_redo_manager = UndoRedoManager.new() undo_redo_manager.connect("undo_redo_state_changed", _on_undo_redo_state_changed) # Connect to existing signals connect("node_added", _on_node_added_for_undo) connect("node_removed", _on_node_removed_for_undo) connect("connection_created", _on_connection_created_for_undo) connect("connection_removed", _on_connection_removed_for_undo) connect("graph_changed", _on_graph_changed_for_undo) # Handle node added for undo func _on_node_added_for_undo(node: BaseGraphNode) -> void: if undo_redo_manager: var operation = undo_redo_manager.create_node_added_operation(node) undo_redo_manager.add_operation(operation) # Handle node removed for undo func _on_node_removed_for_undo(node: BaseGraphNode) -> void: if undo_redo_manager: var operation = undo_redo_manager.create_node_removed_operation(node) undo_redo_manager.add_operation(operation) # Handle connection created for undo func _on_connection_created_for_undo(from_node: String, from_port: int, to_node: String, to_port: int) -> void: if undo_redo_manager: var operation = undo_redo_manager.create_connection_created_operation(from_node, from_port, to_node, to_port) undo_redo_manager.add_operation(operation) # Handle connection removed for undo func _on_connection_removed_for_undo(from_node: String, from_port: int, to_node: String, to_port: int) -> void: if undo_redo_manager: var operation = undo_redo_manager.create_connection_removed_operation(from_node, from_port, to_node, to_port) undo_redo_manager.add_operation(operation) # Handle graph changed for undo func _on_graph_changed_for_undo() -> void: # This could be used for batch operations or complex changes pass # Handle undo/redo state changed func _on_undo_redo_state_changed() -> void: # Update UI to reflect undo/redo availability _update_undo_redo_ui() # Update undo/redo UI func _update_undo_redo_ui() -> void: # This would update toolbar buttons or menu items pass # Perform undo operation func undo() -> void: if undo_redo_manager and undo_redo_manager.can_undo(): undo_redo_manager.undo() # Perform redo operation func redo() -> void: if undo_redo_manager and undo_redo_manager.can_redo(): undo_redo_manager.redo()