progress.

This commit is contained in:
2025-07-31 09:27:26 -07:00
parent a429a3e06b
commit 2edf692ce9
47 changed files with 3263 additions and 2103 deletions

View File

@@ -0,0 +1,634 @@
@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()