@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 # 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.tscn").instantiate() "exit": new_node = preload("res://addons/cutscene_editor/editor/nodes/ExitNode.tscn").instantiate() "move": new_node = preload("res://addons/cutscene_editor/editor/nodes/MoveActionNode.tscn").instantiate() "turn": new_node = preload("res://addons/cutscene_editor/editor/nodes/TurnActionNode.tscn").instantiate() "dialogue": new_node = preload("res://addons/cutscene_editor/editor/nodes/DialogueActionNode.tscn").instantiate() "animation": new_node = preload("res://addons/cutscene_editor/editor/nodes/AnimationActionNode.tscn").instantiate() "wait": new_node = preload("res://addons/cutscene_editor/editor/nodes/WaitActionNode.tscn").instantiate() _: return null # Set node position new_node.position_offset = position # 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) # 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) # 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 _: # 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 # 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) # 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" _: 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: # 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 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") func get_graph_node_by_id(id: String): return find_child(id) # 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["position"]["x"], node_data["position"]["y"])) node.owner=self if node: node.name = node_data["id"] # Set node parameters for param_name in node_data["parameters"]: node.set_parameter(param_name, node_data["parameters"][param_name]) node._parameters_to_view() # Create connections from cutscene data for connection_data in cutscene.connections: print(get_graph_node_by_id(connection_data["from_node"])) 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 = { "id": str(child.node_id), "type": child.node_type, "position": { "x": child.position_offset.x, "y": child.position_offset.y }, "parameters": child.action_parameters } print(node_data) current_cutscene.nodes.append(node_data) # Save connections for connection in get_connection_list(): # Get actual node instances from node names to access node_id var from_node_instance = get_node_or_null(str(connection["from_node"])) var to_node_instance = get_node_or_null(str(connection["to_node"])) # Skip if nodes don't exist (shouldn't happen, but safety check) if not from_node_instance or not to_node_instance: continue var connection_data = { "id": _generate_unique_connection_id(), "from_node": str(from_node_instance.node_id), "from_port": connection["from_port"], "to_node": str(to_node_instance.node_id), "to_port": connection["to_port"] } current_cutscene.connections.append(connection_data) return current_cutscene # Generate a unique ID for connections func _generate_unique_connection_id() -> String: return "conn_" + str(Time.get_ticks_msec()) # 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()