496 lines
15 KiB
GDScript
496 lines
15 KiB
GDScript
@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.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.node_id or connection["to_node"] == node.node_id:
|
|
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")
|
|
|
|
# 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"]))
|
|
if node:
|
|
node.node_id = node_data["id"]
|
|
# 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 = {
|
|
"id": child.node_id,
|
|
"type": child.node_type,
|
|
"position": {
|
|
"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 = {
|
|
"id": _generate_unique_connection_id(),
|
|
"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
|
|
|
|
# Generate a unique ID for connections
|
|
func _generate_unique_connection_id() -> String:
|
|
return "conn_" + str(Time.get_ticks_msec())
|
|
|
|
|
|
# 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()
|