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,215 @@
@tool
extends EditorPlugin
# Main plugin script for the cutscene editor
var dock_panel: Control
var graph_edit: GraphEdit
func _enter_tree() -> void:
# Initialize the plugin when it's enabled
_setup_plugin()
# Add custom types for cutscene resources
add_custom_type("CutsceneResource", "Resource", preload("res://addons/cutscene_editor/editor/resources/CutsceneResource.gd"),
preload("res://addons/cutscene_editor/icons/icon_entry.svg"))
func _exit_tree() -> void:
# Clean up when the plugin is disabled
_cleanup_plugin()
# Remove custom types
remove_custom_type("CutsceneResource")
func _setup_plugin() -> void:
# Create the main dock panel
dock_panel = _create_dock_panel()
# Add the dock panel to the editor
add_control_to_bottom_panel(dock_panel, "Cutscene Editor")
# Register the custom inspector plugin
#var inspector_plugin = preload("res://addons/cutscene_editor/editor/inspectors/CutsceneInspectorPlugin.gd").new()
#add_inspector_plugin(inspector_plugin)
func _cleanup_plugin() -> void:
# Remove the dock panel from the editor
if dock_panel:
remove_control_from_bottom_panel(dock_panel)
dock_panel.queue_free()
# Remove inspector plugin
# Note: Inspector plugins are automatically removed when the plugin is disabled
func _create_dock_panel() -> Control:
# Create the main dock panel UI
var panel = PanelContainer.new()
# Create a main container for the editor
var main_vbox = VBoxContainer.new()
main_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
panel.add_child(main_vbox)
# Create toolbar
var toolbar = _create_toolbar()
main_vbox.add_child(toolbar)
# Create graph edit area
graph_edit = _create_graph_edit()
main_vbox.add_child(graph_edit)
# Set up preview system
_setup_preview_system()
# Set up undo/redo system
_setup_undo_redo_system()
return panel
func _create_toolbar() -> Control:
# Create the main toolbar with common actions
var toolbar = HBoxContainer.new()
# New button
var new_button = Button.new()
new_button.text = "New"
new_button.icon = get_editor_interface().get_base_control().get_theme_icon("New", "EditorIcons")
new_button.connect("pressed", _on_new_pressed)
toolbar.add_child(new_button)
# Open button
var open_button = Button.new()
open_button.text = "Open"
open_button.icon = get_editor_interface().get_base_control().get_theme_icon("Load", "EditorIcons")
open_button.connect("pressed", _on_open_pressed)
toolbar.add_child(open_button)
# Save button
var save_button = Button.new()
save_button.text = "Save"
save_button.icon = get_editor_interface().get_base_control().get_theme_icon("Save", "EditorIcons")
save_button.connect("pressed", _on_save_pressed)
toolbar.add_child(save_button)
# Separator
var separator1 = VSeparator.new()
toolbar.add_child(separator1)
# Undo button
var undo_button = Button.new()
undo_button.text = "Undo"
undo_button.icon = get_editor_interface().get_base_control().get_theme_icon("Undo", "EditorIcons")
undo_button.connect("pressed", _on_undo_pressed)
toolbar.add_child(undo_button)
# Redo button
var redo_button = Button.new()
redo_button.text = "Redo"
redo_button.icon = get_editor_interface().get_base_control().get_theme_icon("Redo", "EditorIcons")
redo_button.connect("pressed", _on_redo_pressed)
toolbar.add_child(redo_button)
# Separator
var separator2 = VSeparator.new()
toolbar.add_child(separator2)
# Preview button
var preview_button = Button.new()
preview_button.text = "Preview"
preview_button.icon = get_editor_interface().get_base_control().get_theme_icon("Play", "EditorIcons")
preview_button.connect("pressed", _on_preview_pressed)
toolbar.add_child(preview_button)
return toolbar
func _create_graph_edit() -> GraphEdit:
# Create the main graph edit component
var graph = preload("res://addons/cutscene_editor/editor/CutsceneGraphEdit.gd").new()
graph.name = "CutsceneGraphEdit"
graph.size_flags_horizontal = Control.SIZE_EXPAND_FILL
graph.size_flags_vertical = Control.SIZE_EXPAND_FILL
# Set up graph properties
graph.set_right_disconnects(true)
graph.set_show_grid(true)
graph.set_snapping_enabled(true)
graph.set_snapping_distance(20)
graph.set_minimap_enabled(true)
graph.set_minimap_size(Vector2(200, 150))
# Connect signals
graph.connect("connection_request", graph._on_connection_request)
graph.connect("disconnection_request", graph._on_disconnection_request)
graph.connect("node_selected", graph._on_node_selected)
graph.connect("node_unselected", graph._on_node_unselected)
graph.connect("popup_request", graph._on_popup_request)
graph.connect("connection_to_empty", graph._on_connection_to_empty)
graph.connect("connection_from_empty", graph._on_connection_from_empty)
return graph
func _setup_preview_system() -> void:
# Set up the preview system
if graph_edit:
graph_edit.setup_preview()
func _setup_undo_redo_system() -> void:
# Set up the undo/redo system
if graph_edit:
graph_edit._setup_undo_redo()
# Toolbar button handlers
func _on_new_pressed() -> void:
if graph_edit:
graph_edit.clear_graph()
func _on_open_pressed() -> void:
# Open file dialog to load a cutscene
var file_dialog = EditorFileDialog.new()
file_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE
file_dialog.access = EditorFileDialog.ACCESS_RESOURCES
file_dialog.filters = ["*.tres", "*.res"]
file_dialog.connect("file_selected", _on_file_selected)
# Add to editor interface
get_editor_interface().get_base_control().add_child(file_dialog)
file_dialog.popup_centered_ratio()
func _on_file_selected(path: String) -> void:
# Load the selected cutscene file
if graph_edit:
var cutscene_resource = load(path)
if cutscene_resource and cutscene_resource is CutsceneResource: #preload("res://addons/cutscene_editor/editor/resources/CutsceneResource.gd"):
graph_edit.load_from_cutscene(cutscene_resource)
func _on_save_pressed() -> void:
# Open file dialog to save a cutscene
var file_dialog = EditorFileDialog.new()
file_dialog.file_mode = EditorFileDialog.FILE_MODE_SAVE_FILE
file_dialog.access = EditorFileDialog.ACCESS_RESOURCES
file_dialog.filters = ["*.tres", "*.res"]
file_dialog.connect("file_selected", _on_save_file_selected)
# Add to editor interface
get_editor_interface().get_base_control().add_child(file_dialog)
file_dialog.popup_centered_ratio()
func _on_save_file_selected(path: String) -> void:
# Save the current cutscene to the selected file
if graph_edit:
var cutscene_resource = graph_edit.save_to_cutscene()
if cutscene_resource:
ResourceSaver.save(cutscene_resource, path)
func _on_undo_pressed() -> void:
if graph_edit:
graph_edit.undo()
func _on_redo_pressed() -> void:
if graph_edit:
graph_edit.redo()
func _on_preview_pressed() -> void:
if graph_edit:
graph_edit.start_preview()

View File

@@ -0,0 +1,59 @@
# Cutscene Editor Plugin
A visual graph-based editor for designing point-and-click adventure game cutscenes in Godot 4.
## Overview
The Cutscene Editor Plugin provides a node-based interface for creating complex cutscene sequences without manual coding. Users can configure narrative sequences through visual connections that mirror the action-attachment functionality of the underlying cutscene system.
## Features
- **Visual Node-Based Interface**: Drag-and-drop nodes for creating cutscenes
- **Action Representation**: Visual nodes for all core action types (Move, Turn, Dialogue, Animation, Wait)
- **Parallel Execution**: Special Parallel Group nodes for simultaneous actions
- **Connection System**: Visual connections for sequential and parallel execution flow
- **Property Editing**: Inline parameter editing within nodes
- **Real-Time Preview**: Immediate visualization of cutscene execution
- **Undo/Redo System**: Comprehensive action reversal capabilities
- **Save/Load Functionality**: Persistent storage of cutscene designs
## Node Types
- **Entry Node**: Starting point for cutscenes (Light Green)
- **Exit Node**: Ending point for cutscenes (Red)
- **MoveAction Node**: Moves characters to specific positions (Blue)
- **TurnAction Node**: Rotates characters to face targets (Green)
- **DialogueAction Node**: Displays character dialogue (Yellow)
- **AnimationAction Node**: Plays character animations (Purple)
- **WaitAction Node**: Creates time delays (Gray)
- **ParallelGroup Node**: Groups actions for simultaneous execution (Orange)
## Installation
1. Download the plugin package
2. Extract the contents to your Godot project's `addons` folder
3. Enable the plugin in Project Settings > Plugins
4. Restart Godot
## Usage
1. **Creating Nodes**: Right-click on the graph canvas and select a node type
2. **Connecting Nodes**: Drag from output connection points to input connection points
3. **Editing Parameters**: Select a node and modify parameters in the Properties Panel
4. **Previewing**: Click the Preview button to see your cutscene in action
5. **Saving**: Use File > Save to store your cutscene design
## Requirements
- Godot Engine 4.3 or later
- OpenGL 3.3 or Vulkan compatible graphics card
## License
This plugin is released under the MIT License. See LICENSE file for details.
## Support
For support, please visit:
- Documentation: [https://example.com/docs](https://example.com/docs)
- Issues: [https://github.com/example/cutscene-editor/issues](https://github.com/example/cutscene-editor/issues)

View File

@@ -0,0 +1,312 @@
@tool
class_name CutsceneGenerator
extends Object
# System for generating executable cutscene code from graph data
# Properties
var graph_nodes: Dictionary = {} # Map of node names to node data
var connections: Array = [] # List of connection data
var logical_connections: Array = [] # Logical connections for parallel groups
var node_instances: Dictionary = {} # Map of node names to instantiated nodes
# Generate a cutscene from graph data
func generate_cutscene(cutscene_resource: CutsceneResource) -> CutsceneManager:
# Clear previous data
graph_nodes.clear()
connections.clear()
logical_connections.clear()
node_instances.clear()
# Load graph data
for node_data in cutscene_resource.nodes:
graph_nodes[node_data["name"]] = node_data
connections = cutscene_resource.connections
# Load logical connections if they exist
if cutscene_resource.has_method("get_parallel_connections"):
logical_connections = cutscene_resource.get_parallel_connections()
# Create CutsceneManager
var cutscene_manager = CutsceneManager.new()
# Find entry node
var entry_node = _find_entry_node()
if not entry_node:
printerr("No entry node found in cutscene")
return cutscene_manager
# Build action sequence starting from entry node
_build_action_sequence(entry_node, cutscene_manager)
return cutscene_manager
# Find the entry node in the graph
func _find_entry_node() -> Dictionary:
for node_name in graph_nodes:
var node_data = graph_nodes[node_name]
if node_data["type"] == "entry":
return node_data
return {}
# Build action sequence from graph data
func _build_action_sequence(start_node: Dictionary, cutscene_manager: CutsceneManager) -> void:
# Use a queue-based traversal to process nodes in order
var node_queue = []
var processed_nodes = {}
var next_connections = _get_outgoing_connections(start_node["name"])
# Add initial connections to queue
for conn in next_connections:
node_queue.append(conn["to_node"])
# Process nodes in order
while not node_queue.is_empty():
var current_node_name = node_queue.pop_front()
# Skip if already processed
if processed_nodes.has(current_node_name):
continue
# Mark as processed
processed_nodes[current_node_name] = true
# Get node data
var node_data = graph_nodes.get(current_node_name, {})
if node_data.is_empty():
continue
# Handle parallel groups specially
if node_data["type"] == "parallel":
_handle_parallel_group(node_data, cutscene_manager, node_queue)
else:
# Create action for regular nodes
var action = _create_action_from_node(node_data)
if action:
cutscene_manager.add_action(action)
# Add next nodes to queue
var outgoing_connections = _get_outgoing_connections(current_node_name)
for conn in outgoing_connections:
if not processed_nodes.has(conn["to_node"]):
node_queue.append(conn["to_node"])
# Handle parallel group nodes
func _handle_parallel_group(parallel_node: Dictionary, cutscene_manager: CutsceneManager, node_queue: Array) -> void:
# Find all nodes connected to this parallel group
var parallel_actions = []
# Look for logical connections to this parallel group
for conn in logical_connections:
if conn["to_node"] == parallel_node["name"] and conn.get("is_parallel", false):
var source_node_name = conn["from_node"]
var source_node_data = graph_nodes.get(source_node_name, {})
if not source_node_data.is_empty():
var action = _create_action_from_node(source_node_data)
if action:
parallel_actions.append(action)
# Add parallel actions to cutscene manager
if not parallel_actions.is_empty():
cutscene_manager.add_parallel_actions(parallel_actions)
# Add nodes connected to parallel group output to queue
var outgoing_connections = _get_outgoing_connections(parallel_node["name"])
for conn in outgoing_connections:
node_queue.append(conn["to_node"])
# Create an action instance from node data
func _create_action_from_node(node_data: Dictionary):
var parameters = node_data["parameters"]
match node_data["type"]:
"move":
var character_path = parameters.get("character", "")
var target_x = parameters.get("target_x", 0.0)
var target_y = parameters.get("target_y", 0.0)
var speed = parameters.get("speed", 100.0)
# In a real implementation, we would resolve the character path to an actual node
# For now, we'll create a placeholder
var character_node = null # This would be resolved at runtime
var target_position = Vector2(target_x, target_y)
return MoveAction.new(character_node, target_position, speed)
"turn":
var character_path = parameters.get("character", "")
var target = parameters.get("target", "")
var turn_speed = parameters.get("turn_speed", 2.0)
# In a real implementation, we would resolve the paths to actual nodes
var character_node = null # This would be resolved at runtime
var target_node = null # This would be resolved at runtime
return TurnAction.new(character_node, target_node, turn_speed)
"dialogue":
var character_path = parameters.get("character", "")
var text = parameters.get("text", "")
var duration = parameters.get("duration", 0.0)
var character_node = null # This would be resolved at runtime
return DialogueAction.new(character_node, text, duration)
"animation":
var character_path = parameters.get("character", "")
var animation_name = parameters.get("animation_name", "")
var loop = parameters.get("loop", false)
var character_node = null # This would be resolved at runtime
return AnimationAction.new(character_node, animation_name, loop)
"wait":
var duration = parameters.get("duration", 1.0)
return WaitAction.new(duration)
_:
printerr("Unknown node type: %s" % node_data["type"])
return null
# Get outgoing connections from a node
func _get_outgoing_connections(node_name: String) -> Array:
var outgoing = []
for conn in connections:
if conn["from_node"] == node_name:
outgoing.append(conn)
return outgoing
# Get incoming connections to a node
func _get_incoming_connections(node_name: String) -> Array:
var incoming = []
for conn in connections:
if conn["to_node"] == node_name:
incoming.append(conn)
return incoming
# Generate GDScript code from graph data (alternative output format)
func generate_gdscript_code(cutscene_resource: CutsceneResource) -> String:
var code = "# Generated Cutscene Code\n"
code += "extends Node2D\n\n"
# Add character variables
code += "# Character nodes\n"
# This would need to be determined from the graph data
code += "@onready var character1: Node2D = $Character1\n"
code += "@onready var character2: Node2D = $Character2\n\n"
code += "# Cutscene manager\n"
code += "var cutscene_manager: CutsceneManager\n\n"
code += "func _ready() -> void:\n"
code += " # Initialize the cutscene system\n"
code += " setup_cutscene()\n"
code += " \n"
code += " # Start the cutscene\n"
code += " cutscene_manager.start()\n\n"
code += "func setup_cutscene() -> void:\n"
code += " # Create the cutscene manager\n"
code += " cutscene_manager = CutsceneManager.new()\n"
code += " add_child(cutscene_manager)\n"
code += " \n"
# Generate action sequence
var action_sequence = _generate_action_sequence_code(cutscene_resource)
code += action_sequence
code += "\n"
code += "func _on_cutscene_completed() -> void:\n"
code += " print(\"Cutscene completed!\")\n"
return code
# Generate action sequence code
func _generate_action_sequence_code(cutscene_resource: CutsceneResource) -> String:
var code = ""
# This is a simplified approach - a real implementation would need to
# properly traverse the graph and handle parallel groups
# Find entry node and traverse from there
var entry_node = _find_entry_node_in_resource(cutscene_resource)
if entry_node.is_empty():
return code
# For demonstration, we'll just generate code for each node in order
# A real implementation would need proper graph traversal
for node_data in cutscene_resource.nodes:
if node_data["type"] == "entry" or node_data["type"] == "exit":
continue
var action_code = _generate_action_code(node_data)
if not action_code.is_empty():
code += " " + action_code + "\n"
return code
# Find entry node in resource
func _find_entry_node_in_resource(cutscene_resource: CutsceneResource) -> Dictionary:
for node_data in cutscene_resource.nodes:
if node_data["type"] == "entry":
return node_data
return {}
# Generate code for a single action
func _generate_action_code(node_data: Dictionary) -> String:
var parameters = node_data["parameters"]
match node_data["type"]:
"move":
var character = parameters.get("character", "character1")
var x = parameters.get("target_x", 0.0)
var y = parameters.get("target_y", 0.0)
var speed = parameters.get("speed", 100.0)
return "cutscene_manager.add_action(MoveAction.new(%s, Vector2(%f, %f), %f))" % [character, x, y, speed]
"turn":
var character = parameters.get("character", "character1")
var target = parameters.get("target", "character2")
var speed = parameters.get("turn_speed", 2.0)
return "cutscene_manager.add_action(TurnAction.new(%s, %s, %f))" % [character, target, speed]
"dialogue":
var character = parameters.get("character", "character1")
var text = parameters.get("text", "")
var duration = parameters.get("duration", 0.0)
# Escape quotes in text
var escaped_text = text.replace("\"", "\\\"")
return "cutscene_manager.add_action(DialogueAction.new(%s, \"%s\", %f))" % [character, escaped_text, duration]
"animation":
var character = parameters.get("character", "character1")
var anim_name = parameters.get("animation_name", "")
var loop = "true" if parameters.get("loop", false) else "false"
return "cutscene_manager.add_action(AnimationAction.new(%s, \"%s\", %s))" % [character, anim_name, loop]
"wait":
var duration = parameters.get("duration", 1.0)
return "cutscene_manager.add_action(WaitAction.new(%f))" % duration
_:
return ""
# Export to different formats
func export_to_format(cutscene_resource: CutsceneResource, format: String) -> String:
match format:
"gdscript":
return generate_gdscript_code(cutscene_resource)
"json":
return _export_to_json(cutscene_resource)
_:
return ""
# Export to JSON format
func _export_to_json(cutscene_resource: CutsceneResource) -> String:
# Convert the resource to JSON
var json = JSON.new()
return json.stringify(cutscene_resource, " ")

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

View File

@@ -0,0 +1,274 @@
@tool
class_name PreviewManager
extends Node
# Manager for real-time cutscene preview
# Signals
signal preview_started()
signal preview_stopped()
signal preview_paused()
signal preview_resumed()
signal preview_progress(progress: float)
signal node_activated(node_name: String)
signal node_completed(node_name: String)
# Properties
var is_previewing: bool = false
var is_paused: bool = false
var current_cutscene: CutsceneManager
var preview_scene: Node2D # The scene being previewed
var preview_characters: Dictionary = {} # Map of character names to nodes
var graph_edit: CutsceneGraphEdit # Reference to the graph editor
# Preview settings
var preview_speed: float = 1.0
var show_debug_info: bool = true
# Initialize the preview manager
func _init() -> void:
name = "PreviewManager"
# Set up the preview with a graph editor
func setup_preview(graph: CutsceneGraphEdit) -> void:
graph_edit = graph
# Connect to graph change signals
if graph_edit:
graph_edit.connect("graph_changed", _on_graph_changed)
# Start the preview
func start_preview() -> void:
if is_previewing:
return
# Create preview scene
_setup_preview_scene()
# Generate cutscene from current graph
var cutscene_resource = graph_edit.save_to_cutscene()
var generator = CutsceneGenerator.new()
current_cutscene = generator.generate_cutscene(cutscene_resource)
# Add cutscene to preview scene
if preview_scene and current_cutscene:
preview_scene.add_child(current_cutscene)
# Connect to cutscene signals for feedback
_connect_cutscene_signals()
# Start the cutscene
current_cutscene.start()
# Update state
is_previewing = true
is_paused = false
emit_signal("preview_started")
# Stop the preview
func stop_preview() -> void:
if not is_previewing:
return
# Stop the cutscene
if current_cutscene:
current_cutscene.stop()
_disconnect_cutscene_signals()
# Remove from preview scene
if current_cutscene.get_parent() == preview_scene:
preview_scene.remove_child(current_cutscene)
current_cutscene.queue_free()
# Clean up preview scene
_cleanup_preview_scene()
# Update state
is_previewing = false
is_paused = false
emit_signal("preview_stopped")
# Pause the preview
func pause_preview() -> void:
if not is_previewing or is_paused:
return
if current_cutscene:
current_cutscene.pause()
is_paused = true
emit_signal("preview_paused")
# Resume the preview
func resume_preview() -> void:
if not is_previewing or not is_paused:
return
if current_cutscene:
current_cutscene.resume()
is_paused = false
emit_signal("preview_resumed")
# Set preview speed
func set_preview_speed(speed: float) -> void:
preview_speed = speed
# In a real implementation, this would affect the time scale
# of the preview scene
# Set up the preview scene
func _setup_preview_scene() -> void:
# Create or reuse preview scene
if not preview_scene:
preview_scene = Node2D.new()
preview_scene.name = "PreviewScene"
add_child(preview_scene)
# Set up characters for preview
_setup_preview_characters()
# Set up characters for preview
func _setup_preview_characters() -> void:
# Clear existing characters
preview_characters.clear()
# Create placeholder characters for preview
# In a real implementation, this would load actual character scenes
var character1 = _create_preview_character("Character1", Vector2(100, 100))
var character2 = _create_preview_character("Character2", Vector2(200, 100))
preview_characters["Character1"] = character1
preview_characters["Character2"] = character2
# Add to preview scene
if preview_scene:
preview_scene.add_child(character1)
preview_scene.add_child(character2)
# Create a preview character
func _create_preview_character(name: String, position: Vector2) -> Node2D:
var character = Node2D.new()
character.name = name
character.position = position
# Add a visual representation
var sprite = Polygon2D.new()
sprite.polygon = PackedVector2Array([
Vector2(-10, -20),
Vector2(-30, 0),
Vector2(-10, 20),
Vector2(10, 20),
Vector2(30, 0),
Vector2(10, -20)
])
sprite.color = Color(0.5, 0.7, 1.0)
character.add_child(sprite)
return character
# Clean up the preview scene
func _cleanup_preview_scene() -> void:
# Remove characters
for char_name in preview_characters:
var character = preview_characters[char_name]
if character.get_parent() == preview_scene:
preview_scene.remove_child(character)
character.queue_free()
preview_characters.clear()
# Connect to cutscene signals for feedback
func _connect_cutscene_signals() -> void:
if not current_cutscene:
return
# Disconnect existing connections
_disconnect_cutscene_signals()
# Connect to signals
current_cutscene.connect("cutscene_started", _on_cutscene_started)
current_cutscene.connect("cutscene_completed", _on_cutscene_completed)
current_cutscene.connect("cutscene_paused", _on_cutscene_paused)
current_cutscene.connect("cutscene_resumed", _on_cutscene_resumed)
current_cutscene.connect("action_started", _on_action_started)
current_cutscene.connect("action_completed", _on_action_completed)
# Disconnect from cutscene signals
func _disconnect_cutscene_signals() -> void:
if not current_cutscene:
return
if current_cutscene.is_connected("cutscene_started", _on_cutscene_started):
current_cutscene.disconnect("cutscene_started", _on_cutscene_started)
if current_cutscene.is_connected("cutscene_completed", _on_cutscene_completed):
current_cutscene.disconnect("cutscene_completed", _on_cutscene_completed)
if current_cutscene.is_connected("cutscene_paused", _on_cutscene_paused):
current_cutscene.disconnect("cutscene_paused", _on_cutscene_paused)
if current_cutscene.is_connected("cutscene_resumed", _on_cutscene_resumed):
current_cutscene.disconnect("cutscene_resumed", _on_cutscene_resumed)
if current_cutscene.is_connected("action_started", _on_action_started):
current_cutscene.disconnect("action_started", _on_action_started)
if current_cutscene.is_connected("action_completed", _on_action_completed):
current_cutscene.disconnect("action_completed", _on_action_completed)
# Handle graph changes
func _on_graph_changed() -> void:
# If we're previewing, restart the preview to reflect changes
if is_previewing:
stop_preview()
start_preview()
# Handle cutscene started
func _on_cutscene_started() -> void:
emit_signal("preview_started")
# Handle cutscene completed
func _on_cutscene_completed() -> void:
emit_signal("preview_stopped")
is_previewing = false
is_paused = false
# Handle cutscene paused
func _on_cutscene_paused() -> void:
emit_signal("preview_paused")
is_paused = true
# Handle cutscene resumed
func _on_cutscene_resumed() -> void:
emit_signal("preview_resumed")
is_paused = false
# Handle action started
func _on_action_started(action: Action) -> void:
# Find the corresponding node in the graph and highlight it
var node_name = _find_node_for_action(action)
if node_name:
emit_signal("node_activated", node_name)
# Update graph editor visualization
if graph_edit:
var node = graph_edit.get_node_or_null(node_name)
if node and node.has_method("set_state"):
node.set_state(BaseGraphNode.NodeState.ACTIVE)
# Handle action completed
func _on_action_completed(action: Action) -> void:
# Find the corresponding node in the graph and mark as completed
var node_name = _find_node_for_action(action)
if node_name:
emit_signal("node_completed", node_name)
# Update graph editor visualization
if graph_edit:
var node = graph_edit.get_node_or_null(node_name)
if node and node.has_method("set_state"):
node.set_state(BaseGraphNode.NodeState.COMPLETED)
# Find node corresponding to an action
func _find_node_for_action(action: Action) -> String:
# This would need to map actions to nodes
# In a real implementation, we would store this mapping when generating the cutscene
# For now, we'll return a placeholder
return ""
# Get the preview scene for display
func get_preview_scene() -> Node2D:
return preview_scene

View File

@@ -0,0 +1,186 @@
@tool
class_name PreviewPanel
extends PanelContainer
# UI panel for displaying the preview
# UI elements
var preview_viewport: SubViewport
var preview_texture: TextureRect
var controls_container: HBoxContainer
var play_button: Button
var pause_button: Button
var stop_button: Button
var speed_slider: HSlider
var progress_bar: ProgressBar
var debug_label: Label
# Properties
var preview_manager: PreviewManager
var is_playing: bool = false
# Initialize the preview panel
func _ready() -> void:
_setup_ui()
_setup_signals()
# Set up the UI
func _setup_ui() -> void:
# Main container
var main_vbox = VBoxContainer.new()
main_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(main_vbox)
# Preview viewport
var viewport_container = AspectRatioContainer.new()
viewport_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
viewport_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
viewport_container.ratio = 16.0/9.0
main_vbox.add_child(viewport_container)
preview_viewport = SubViewport.new()
preview_viewport.size = Vector2i(800, 450)
preview_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
viewport_container.add_child(preview_viewport)
preview_texture = TextureRect.new()
preview_texture.expand = true
preview_texture.size_flags_horizontal = Control.SIZE_EXPAND_FILL
preview_texture.size_flags_vertical = Control.SIZE_EXPAND_FILL
preview_texture.texture = preview_viewport.get_texture()
viewport_container.add_child(preview_texture)
# Controls
controls_container = HBoxContainer.new()
controls_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_vbox.add_child(controls_container)
play_button = Button.new()
play_button.text = ""
play_button.flat = true
controls_container.add_child(play_button)
pause_button = Button.new()
pause_button.text = ""
pause_button.flat = true
controls_container.add_child(pause_button)
stop_button = Button.new()
stop_button.text = ""
stop_button.flat = true
controls_container.add_child(stop_button)
var speed_label = Label.new()
speed_label.text = "Speed:"
controls_container.add_child(speed_label)
speed_slider = HSlider.new()
speed_slider.min = 0.1
speed_slider.max = 2.0
speed_slider.step = 0.1
speed_slider.value = 1.0
speed_slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
controls_container.add_child(speed_slider)
# Progress bar
progress_bar = ProgressBar.new()
progress_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
progress_bar.percent_visible = true
main_vbox.add_child(progress_bar)
# Debug info
debug_label = Label.new()
debug_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_vbox.add_child(debug_label)
# Set up signal connections
func _setup_signals() -> void:
play_button.connect("pressed", _on_play_pressed)
pause_button.connect("pressed", _on_pause_pressed)
stop_button.connect("pressed", _on_stop_pressed)
speed_slider.connect("value_changed", _on_speed_changed)
# Set the preview manager
func set_preview_manager(manager: PreviewManager) -> void:
preview_manager = manager
# Connect to preview manager signals
if preview_manager:
preview_manager.connect("preview_started", _on_preview_started)
preview_manager.connect("preview_stopped", _on_preview_stopped)
preview_manager.connect("preview_paused", _on_preview_paused)
preview_manager.connect("preview_resumed", _on_preview_resumed)
preview_manager.connect("preview_progress", _on_preview_progress)
preview_manager.connect("node_activated", _on_node_activated)
preview_manager.connect("node_completed", _on_node_completed)
# Set the graph editor
func set_graph_edit(graph_edit: CutsceneGraphEdit) -> void:
if preview_manager:
preview_manager.setup_preview(graph_edit)
# Add preview scene to viewport
var preview_scene = preview_manager.get_preview_scene()
if preview_scene and preview_scene.get_parent() != preview_viewport:
preview_viewport.add_child(preview_scene)
# Handle play button press
func _on_play_pressed() -> void:
if preview_manager:
if is_playing:
preview_manager.resume_preview()
else:
preview_manager.start_preview()
# Handle pause button press
func _on_pause_pressed() -> void:
if preview_manager:
preview_manager.pause_preview()
# Handle stop button press
func _on_stop_pressed() -> void:
if preview_manager:
preview_manager.stop_preview()
# Handle speed slider change
func _on_speed_changed(value: float) -> void:
if preview_manager:
preview_manager.set_preview_speed(value)
# Handle preview started
func _on_preview_started() -> void:
is_playing = true
play_button.text = ""
debug_label.text = "Preview started"
# Handle preview stopped
func _on_preview_stopped() -> void:
is_playing = false
play_button.text = ""
progress_bar.value = 0
debug_label.text = "Preview stopped"
# Handle preview paused
func _on_preview_paused() -> void:
is_playing = false
play_button.text = ""
debug_label.text = "Preview paused"
# Handle preview resumed
func _on_preview_resumed() -> void:
is_playing = true
play_button.text = ""
debug_label.text = "Preview resumed"
# Handle preview progress
func _on_preview_progress(progress: float) -> void:
progress_bar.value = progress * 100
# Handle node activated
func _on_node_activated(node_name: String) -> void:
debug_label.text = "Executing: %s" % node_name
# Handle node completed
func _on_node_completed(node_name: String) -> void:
debug_label.text = "Completed: %s" % node_name

View File

@@ -0,0 +1,246 @@
@tool
class_name UndoRedoManager
extends Object
# Manager for undo/redo operations
# Signals
signal undo_redo_state_changed()
# Properties
var undo_stack: Array = [] # Stack of undo operations
var redo_stack: Array = [] # Stack of redo operations
var max_history: int = 100 # Maximum number of operations to store
var is_processing: bool = false # Flag to prevent recursive processing
# Operation types
enum OperationType {
NODE_ADDED,
NODE_REMOVED,
NODE_MOVED,
NODE_PROPERTY_CHANGED,
CONNECTION_CREATED,
CONNECTION_REMOVED,
GRAPH_CLEARED
}
# Initialize the undo/redo manager
func _init() -> void:
pass
# Add an operation to the undo stack
func add_operation(operation: Dictionary) -> void:
if is_processing:
return
# Add to undo stack
undo_stack.push_back(operation)
# Limit stack size
if undo_stack.size() > max_history:
undo_stack.pop_front()
# Clear redo stack when new operation is added
redo_stack.clear()
# Emit signal
emit_signal("undo_redo_state_changed")
# Perform undo operation
func undo() -> void:
if undo_stack.is_empty():
return
# Set processing flag to prevent recursive calls
is_processing = true
# Get last operation
var operation = undo_stack.pop_back()
# Perform reverse operation
_perform_reverse_operation(operation)
# Add to redo stack
redo_stack.push_back(operation)
# Limit redo stack size
if redo_stack.size() > max_history:
redo_stack.pop_front()
# Clear processing flag
is_processing = false
# Emit signal
emit_signal("undo_redo_state_changed")
# Perform redo operation
func redo() -> void:
if redo_stack.is_empty():
return
# Set processing flag to prevent recursive calls
is_processing = true
# Get last redo operation
var operation = redo_stack.pop_back()
# Perform original operation
_perform_operation(operation)
# Add to undo stack
undo_stack.push_back(operation)
# Limit undo stack size
if undo_stack.size() > max_history:
undo_stack.pop_front()
# Clear processing flag
is_processing = false
# Emit signal
emit_signal("undo_redo_state_changed")
# Check if undo is possible
func can_undo() -> bool:
return not undo_stack.is_empty()
# Check if redo is possible
func can_redo() -> bool:
return not redo_stack.is_empty()
# Clear all history
func clear_history() -> void:
undo_stack.clear()
redo_stack.clear()
emit_signal("undo_redo_state_changed")
# Perform an operation
func _perform_operation(operation: Dictionary) -> void:
# This would be implemented in the graph editor
pass
# Perform the reverse of an operation
func _perform_reverse_operation(operation: Dictionary) -> void:
match operation["type"]:
OperationType.NODE_ADDED:
_reverse_node_added(operation)
OperationType.NODE_REMOVED:
_reverse_node_removed(operation)
OperationType.NODE_MOVED:
_reverse_node_moved(operation)
OperationType.NODE_PROPERTY_CHANGED:
_reverse_node_property_changed(operation)
OperationType.CONNECTION_CREATED:
_reverse_connection_created(operation)
OperationType.CONNECTION_REMOVED:
_reverse_connection_removed(operation)
OperationType.GRAPH_CLEARED:
_reverse_graph_cleared(operation)
# Create operation for node added
func create_node_added_operation(node: BaseGraphNode) -> Dictionary:
return {
"type": OperationType.NODE_ADDED,
"node_name": node.name,
"node_type": node.node_type,
"position": node.position_offset,
"parameters": node.action_parameters.duplicate(true)
}
# Reverse node added operation
func _reverse_node_added(operation: Dictionary) -> void:
# This would remove the node in the graph editor
pass
# Create operation for node removed
func create_node_removed_operation(node: BaseGraphNode) -> Dictionary:
return {
"type": OperationType.NODE_REMOVED,
"node_name": node.name,
"node_type": node.node_type,
"position": node.position_offset,
"parameters": node.action_parameters.duplicate(true),
"connections": _get_node_connections(node.name)
}
# Reverse node removed operation
func _reverse_node_removed(operation: Dictionary) -> void:
# This would add the node back in the graph editor
pass
# Create operation for node moved
func create_node_moved_operation(node_name: String, old_position: Vector2, new_position: Vector2) -> Dictionary:
return {
"type": OperationType.NODE_MOVED,
"node_name": node_name,
"old_position": old_position,
"new_position": new_position
}
# Reverse node moved operation
func _reverse_node_moved(operation: Dictionary) -> void:
# This would move the node back to its old position in the graph editor
pass
# Create operation for node property changed
func create_node_property_changed_operation(node_name: String, property_name: String, old_value, new_value) -> Dictionary:
return {
"type": OperationType.NODE_PROPERTY_CHANGED,
"node_name": node_name,
"property_name": property_name,
"old_value": old_value,
"new_value": new_value
}
# Reverse node property changed operation
func _reverse_node_property_changed(operation: Dictionary) -> void:
# This would restore the old property value in the graph editor
pass
# Create operation for connection created
func create_connection_created_operation(from_node: String, from_port: int, to_node: String, to_port: int) -> Dictionary:
return {
"type": OperationType.CONNECTION_CREATED,
"from_node": from_node,
"from_port": from_port,
"to_node": to_node,
"to_port": to_port
}
# Reverse connection created operation
func _reverse_connection_created(operation: Dictionary) -> void:
# This would remove the connection in the graph editor
pass
# Create operation for connection removed
func create_connection_removed_operation(from_node: String, from_port: int, to_node: String, to_port: int) -> Dictionary:
return {
"type": OperationType.CONNECTION_REMOVED,
"from_node": from_node,
"from_port": from_port,
"to_node": to_node,
"to_port": to_port
}
# Reverse connection removed operation
func _reverse_connection_removed(operation: Dictionary) -> void:
# This would recreate the connection in the graph editor
pass
# Create operation for graph cleared
func create_graph_cleared_operation(nodes: Array, connections: Array) -> Dictionary:
return {
"type": OperationType.GRAPH_CLEARED,
"nodes": nodes.duplicate(true),
"connections": connections.duplicate(true)
}
# Reverse graph cleared operation
func _reverse_graph_cleared(operation: Dictionary) -> void:
# This would restore all nodes and connections in the graph editor
pass
# Get connections for a node
func _get_node_connections(node_name: String) -> Array:
# This would retrieve all connections to/from the node
return []

View File

@@ -0,0 +1,62 @@
@tool
class_name AnimationActionNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for AnimationAction
func _init() -> void:
node_type = "animation"
node_id = "animation_" + str(randi())
title = "Animation"
modulate = Color(0.8, 0.4, 0.8) # Purple
# One input and one output connection
var slot = 0
set_slot(slot, true, 0, Color(0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Initialize default parameters
action_parameters["character"] = ""
action_parameters["animation_name"] = ""
action_parameters["loop"] = false
func _setup_parameter_fields() -> void:
# Character field
var char_label = Label.new()
char_label.text = "Character:"
add_child(char_label)
var char_field = LineEdit.new()
char_field.text = action_parameters["character"]
char_field.connect("text_changed", _on_character_changed)
add_child(char_field)
# Animation name field
var anim_label = Label.new()
anim_label.text = "Animation:"
add_child(anim_label)
var anim_field = LineEdit.new()
anim_field.text = action_parameters["animation_name"]
anim_field.connect("text_changed", _on_animation_changed)
add_child(anim_field)
# Loop checkbox
var loop_label = Label.new()
loop_label.text = "Loop:"
add_child(loop_label)
var loop_checkbox = CheckBox.new()
loop_checkbox.button_pressed = action_parameters["loop"]
loop_checkbox.connect("toggled", _on_loop_toggled)
add_child(loop_checkbox)
func _on_character_changed(new_text: String) -> void:
set_parameter("character", new_text)
func _on_animation_changed(new_text: String) -> void:
set_parameter("animation_name", new_text)
func _on_loop_toggled(button_pressed: bool) -> void:
set_parameter("loop", button_pressed)

View File

@@ -0,0 +1,284 @@
@tool
class_name BaseGraphNode
extends GraphNode
# Base class for all cutscene graph nodes
# Signals
signal node_selected2(node)
signal node_deleted(node)
signal parameter_changed(node, parameter_name, new_value)
# Node states
enum NodeState {
IDLE,
ACTIVE,
COMPLETED,
ERROR,
PAUSED
}
# Properties
var node_type: String = "base"
var node_id: String # Unique identifier for the node
var action_parameters: Dictionary = {} # Stores parameter values
var current_state: int = NodeState.IDLE
var error_message: String = ""
var property_editors: Dictionary = {} # Map of property names to editors
# Visual feedback elements
var state_border: ColorRect
var overlay_icon: TextureRect
var checkmark_texture: Texture2D
var error_texture: Texture2D
var pause_texture: Texture2D
# Called when the node is ready
func _ready() -> void:
# Set up common node properties
connect("dragged", _on_node_dragged)
connect("selected", _on_node_selected)
# Add close butto
# Set up visual feedback elements
_setup_visual_feedback()
# Add parameter fields based on node type
_setup_parameter_fields()
# Set up visual feedback elements
func _setup_visual_feedback() -> void:
# Create state border
state_border = ColorRect.new()
state_border.name = "StateBorder"
state_border.anchor_right = 1
state_border.anchor_bottom = 1
state_border.margin_left = -2
state_border.margin_top = -2
state_border.margin_right = 2
state_border.margin_bottom = 2
state_border.color = Color(0, 0, 0, 0) # Transparent by default
state_border.z_index = -1 # Behind the node
add_child(state_border)
# Create overlay icon container
var overlay_container = CenterContainer.new()
overlay_container.name = "OverlayContainer"
overlay_container.anchor_right = 1
overlay_container.anchor_bottom = 1
overlay_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(overlay_container)
# Create overlay icon
overlay_icon = TextureRect.new()
overlay_icon.name = "OverlayIcon"
overlay_icon.expand = true
overlay_icon.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
overlay_icon.size_flags_vertical = Control.SIZE_SHRINK_CENTER
overlay_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
overlay_container.add_child(overlay_icon)
# Load textures (these would be actual textures in a real implementation)
# checkmark_texture = preload("res://addons/cutscene_editor/icons/checkmark.png")
# error_texture = preload("res://addons/cutscene_editor/icons/error.png")
# pause_texture = preload("res://addons/cutscene_editor/icons/pause.png")
# Set up parameter fields based on node type
func _setup_parameter_fields() -> void:
# This method should be overridden by subclasses
pass
# Set node state and update visual feedback
func set_state(new_state: int, error_msg: String = "") -> void:
current_state = new_state
error_message = error_msg
_update_visual_feedback()
# Update visual feedback based on current state
func _update_visual_feedback() -> void:
match current_state:
NodeState.IDLE:
_set_idle_state()
NodeState.ACTIVE:
_set_active_state()
NodeState.COMPLETED:
_set_completed_state()
NodeState.ERROR:
_set_error_state()
NodeState.PAUSED:
_set_paused_state()
# Set idle state visuals
func _set_idle_state() -> void:
# Reset to default appearance
state_border.color = Color(0, 0, 0, 0)
overlay_icon.texture = null
modulate = Color(1, 1, 1, 1)
scale = Vector2(1, 1)
# Stop any animations
_stop_animations()
# Set active state visuals
func _set_active_state() -> void:
# White border highlight
state_border.color = Color(1, 1, 1, 1)
state_border.size = Vector2(2, 2)
# Clear overlay
overlay_icon.texture = null
# Start pulsing animation
_start_pulsing_animation()
# Slight scale increase
scale = Vector2(1.05, 1.05)
# Set completed state visuals
func _set_completed_state() -> void:
# Green border
state_border.color = Color(0, 1, 0, 0.5)
state_border.size = Vector2(2, 2)
# Checkmark overlay
overlay_icon.texture = checkmark_texture
overlay_icon.modulate = Color(0, 1, 0, 0.7)
# Slight transparency
modulate = Color(1, 1, 1, 0.8)
# Stop animations
_stop_animations()
# Set error state visuals
func _set_error_state() -> void:
# Red border
state_border.color = Color(1, 0, 0, 1)
state_border.size = Vector2(3, 3)
# Error icon overlay
overlay_icon.texture = error_texture
overlay_icon.modulate = Color(1, 0, 0, 1)
# Red tint
modulate = Color(1, 0.7, 0.7, 1)
# Start shake animation
_start_shake_animation()
# Set paused state visuals
func _set_paused_state() -> void:
# Yellow border
state_border.color = Color(1, 1, 0, 1)
state_border.size = Vector2(2, 2)
# Pause icon overlay
overlay_icon.texture = pause_texture
overlay_icon.modulate = Color(1, 1, 0, 1)
# Stop animations
_stop_animations()
# Animation functions
func _start_pulsing_animation() -> void:
# Create animation player for pulsing effect
var anim_player = AnimationPlayer.new()
anim_player.name = "StateAnimationPlayer"
# Remove existing animation player if present
var existing_player = get_node_or_null("StateAnimationPlayer")
if existing_player:
remove_child(existing_player)
existing_player.queue_free()
add_child(anim_player)
# Create pulsing animation
var animation = Animation.new()
animation.name = "pulse"
animation.length = 1.0
animation.loop_mode = Animation.LOOP_LINEAR
# Scale property track
var scale_track = animation.add_track(Animation.TYPE_VALUE)
animation.track_set_path(scale_track, ".:scale")
animation.track_insert_key(scale_track, 0.0, Vector2(1.05, 1.05))
animation.track_insert_key(scale_track, 0.5, Vector2(1.1, 1.1))
animation.track_insert_key(scale_track, 1.0, Vector2(1.05, 1.05))
anim_player.add_animation("pulse", animation)
anim_player.play("pulse")
func _start_shake_animation() -> void:
# Create animation player for shake effect
var anim_player = AnimationPlayer.new()
anim_player.name = "StateAnimationPlayer"
# Remove existing animation player if present
var existing_player = get_node_or_null("StateAnimationPlayer")
if existing_player:
remove_child(existing_player)
existing_player.queue_free()
add_child(anim_player)
# Create shake animation
var animation = Animation.new()
animation.name = "shake"
animation.length = 0.5
animation.loop_mode = Animation.LOOP_LINEAR
# Position property track
var position_track = animation.add_track(Animation.TYPE_VALUE)
animation.track_set_path(position_track, ".:position_offset")
animation.track_insert_key(position_track, 0.0, position_offset)
animation.track_insert_key(position_track, 0.1, position_offset + Vector2(2, 0))
animation.track_insert_key(position_track, 0.2, position_offset + Vector2(-2, 0))
animation.track_insert_key(position_track, 0.3, position_offset + Vector2(0, 2))
animation.track_insert_key(position_track, 0.4, position_offset + Vector2(0, -2))
animation.track_insert_key(position_track, 0.5, position_offset)
anim_player.add_animation("shake", animation)
anim_player.play("shake")
func _stop_animations() -> void:
var anim_player = get_node_or_null("StateAnimationPlayer")
if anim_player:
anim_player.stop()
# Update parameter value
func set_parameter(parameter_name: String, value) -> void:
action_parameters[parameter_name] = value
# Update editor if it exists
if property_editors.has(parameter_name):
var editor = property_editors[parameter_name]
editor.set_value(value)
emit_signal("parameter_changed", self, parameter_name, value)
# Get parameter value
func get_parameter(parameter_name: String):
return action_parameters.get(parameter_name, null)
# Handle node dragging
func _on_node_dragged(from: Vector2, to: Vector2) -> void:
# Update position
position_offset = to
# Handle node selection
func _on_node_selected() -> void:
emit_signal("node_selected2", self)
# Clear parameter fields
func _clear_parameter_fields() -> void:
# Remove existing editors
for param_name in property_editors:
var editor = property_editors[param_name]
if editor.get_parent() == self:
remove_child(editor)
editor.queue_free()
property_editors.clear()

View File

@@ -0,0 +1,68 @@
@tool
class_name DialogueActionNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for DialogueAction
func _init() -> void:
node_type = "dialogue"
node_id = "dialogue_" + str(randi())
title = "Dialogue"
modulate = Color(1.0, 1.0, 0.5) # Yellow
resizable=true
# One input and one output connection
var slot = 0
set_slot(slot, true, 0, Color(0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Initialize default parameters
action_parameters["character"] = ""
action_parameters["text"] = ""
action_parameters["duration"] = 0.0
func _setup_parameter_fields() -> void:
# Character field
var x = VBoxContainer.new()
add_child(x)
var char_label = Label.new()
char_label.text = "Character:"
char_label.hide()
x.add_child(char_label)
var char_field = LineEdit.new()
char_field.text = action_parameters["character"]
char_field.connect("text_changed", _on_character_changed)
x.add_child(char_field)
# Text field
var text_label = Label.new()
text_label.text = "Text:"
x.add_child(text_label)
var text_field = TextEdit.new()
text_field.text = action_parameters["text"]
text_field.size_flags_vertical = Control.SIZE_EXPAND_FILL
text_field.connect("text_changed", _on_text_changed)
x.add_child(text_field)
# Duration field
var duration_label = Label.new()
duration_label.text = "Duration:"
x.add_child(duration_label)
var duration_field = LineEdit.new()
duration_field.text = str(action_parameters["duration"])
duration_field.connect("text_changed", _on_duration_changed)
x.add_child(duration_field)
func _on_character_changed(new_text: String) -> void:
set_parameter("character", new_text)
func _on_text_changed() -> void:
var text_edit = get_child(get_child_count() - 2) # TextEdit is second to last child
set_parameter("text", text_edit.text)
func _on_duration_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 0.0
set_parameter("duration", value)

View File

@@ -0,0 +1,20 @@
@tool
class_name EntryNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Entry point for the cutscene
func _init() -> void:
node_type = "entry"
node_id = "entry_" + str(randi())
title = "Start"
modulate = Color(0.5, 1.0, 0.5) # Light green
# Entry node has no input connections
# Add one output connection point
var output_slot = 0
set_slot(output_slot, false, 0, Color(0, 0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Entry node has no parameter fields

View File

@@ -0,0 +1,20 @@
@tool
class_name ExitNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Exit point for the cutscene
func _init() -> void:
node_type = "exit"
node_id = "exit_" + str(randi())
title = "End"
modulate = Color(1.0, 0.5, 0.5) # Light red
# Exit node has one input connection
# No output connections
var input_slot = 0
set_slot(input_slot, true, 0, Color(0, 0, 0), false, 0, Color(0, 0, 0, 0))
func _ready() -> void:
super._ready()
# Exit node has no parameter fields

View File

@@ -0,0 +1,88 @@
@tool
class_name MoveActionNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for MoveAction
func _init() -> void:
node_type = "move"
node_id = "move_" + str(randi())
title = "Move"
modulate = Color(0.4, 0.6, 1.0) # Blue
# One input and one output connection
var slot = 0
set_slot(slot, true, 0, Color(0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Initialize default parameters
action_parameters["character"] = ""
action_parameters["target_x"] = 0.0
action_parameters["target_y"] = 0.0
action_parameters["speed"] = 100.0
func _setup_parameter_fields() -> void:
# Character field
var char_label = Label.new()
char_label.text = "Character:"
add_child(char_label)
var char_field = LineEdit.new()
char_field.text = action_parameters["character"]
char_field.connect("text_changed", _on_character_changed)
add_child(char_field)
# Target position fields
var pos_label = Label.new()
pos_label.text = "Target Position:"
add_child(pos_label)
var pos_container = HBoxContainer.new()
var x_label = Label.new()
x_label.text = "X:"
pos_container.add_child(x_label)
var x_field = LineEdit.new()
x_field.text = str(action_parameters["target_x"])
x_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
x_field.connect("text_changed", _on_target_x_changed)
pos_container.add_child(x_field)
var y_label = Label.new()
y_label.text = "Y:"
pos_container.add_child(y_label)
var y_field = LineEdit.new()
y_field.text = str(action_parameters["target_y"])
y_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
y_field.connect("text_changed", _on_target_y_changed)
pos_container.add_child(y_field)
add_child(pos_container)
# Speed field
var speed_label = Label.new()
speed_label.text = "Speed:"
add_child(speed_label)
var speed_field = LineEdit.new()
speed_field.text = str(action_parameters["speed"])
speed_field.connect("text_changed", _on_speed_changed)
add_child(speed_field)
func _on_character_changed(new_text: String) -> void:
set_parameter("character", new_text)
func _on_target_x_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 0.0
set_parameter("target_x", value)
func _on_target_y_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 0.0
set_parameter("target_y", value)
func _on_speed_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 100.0
set_parameter("speed", value)

View File

@@ -0,0 +1,126 @@
@tool
class_name ParallelGroupNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for grouping parallel actions
# Special properties for parallel groups
var input_connections: int = 3 # Number of input connection points
var child_nodes: Array = [] # Child nodes contained within this group
var is_container: bool = true # Flag to indicate this is a container node
# Visual properties
var container_rect: PanelContainer # Visual container for child nodes
func _init() -> void:
node_type = "parallel"
node_id = "parallel_" + str(randi())
title = "Parallel Group"
modulate = Color(1.0, 0.6, 0.2) # Orange
# Set up slots for connections
_setup_slots()
# Set up container for child nodes
_setup_container()
# Set up connection slots
func _setup_slots() -> void:
# Multiple input connections for parallel actions
for i in range(input_connections):
set_slot(i, true, 0, Color(0, 0, 0), false, 0, Color(0, 0, 0, 0))
# Single output connection for sequential continuation
var output_slot = input_connections
set_slot(output_slot, false, 0, Color(0, 0, 0, 0), true, 0, Color(0, 0, 0))
# Set up visual container for child nodes
func _setup_container() -> void:
# Create container panel
container_rect = PanelContainer.new()
container_rect.name = "Container"
container_rect.anchor_right = 1
container_rect.anchor_bottom = 1
container_rect.margin_top = 30 # Leave space for title bar
container_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
# Set container style
var style = StyleBoxFlat.new()
style.bg_color = Color(0.9, 0.9, 0.9, 0.3)
style.border_color = Color(0.5, 0.5, 0.5, 0.5)
style.border_width_left = 1
style.border_width_top = 1
style.border_width_right = 1
style.border_width_bottom = 1
container_rect.add_theme_stylebox_override("panel", style)
add_child(container_rect)
# Create container for child nodes
var child_container = VBoxContainer.new()
child_container.name = "ChildContainer"
child_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
container_rect.add_child(child_container)
# Add a child node to this parallel group
func add_child_node(child_node: BaseGraphNode) -> void:
# Add to child nodes array
child_nodes.append(child_node)
# Add as child in scene tree
if container_rect and container_rect.has_node("ChildContainer"):
container_rect.get_node("ChildContainer").add_child(child_node)
# Update visual representation
_update_container_size()
# Remove a child node from this parallel group
func remove_child_node(child_node: BaseGraphNode) -> void:
# Remove from child nodes array
child_nodes.erase(child_node)
# Remove from scene tree
if child_node.get_parent() == container_rect.get_node("ChildContainer"):
container_rect.get_node("ChildContainer").remove_child(child_node)
# Update visual representation
_update_container_size()
# Update container size based on child nodes
func _update_container_size() -> void:
# Calculate required size based on child nodes
var required_height = 20 # Minimum height
if container_rect and container_rect.has_node("ChildContainer"):
var child_container = container_rect.get_node("ChildContainer")
for child in child_container.get_children():
if child is BaseGraphNode:
required_height += child.size.y + 5 # Add spacing
# Update container size
container_rect.custom_minimum_size.y = required_height
# Handle node dragging
func _on_node_dragged(from: Vector2, to: Vector2) -> void:
# Update position
position_offset = to
# Update child nodes if they're positioned relative to this node
for child in child_nodes:
# Child nodes should move with the parallel group
pass
# Add more input connections if needed
func add_input_connection() -> void:
var slot_index = input_connections
set_slot(slot_index, true, 0, Color(0, 0, 0), false, 0, Color(0, 0, 0, 0))
input_connections += 1
# Get the output slot index
func get_output_slot_index() -> int:
return input_connections
# Check if a node can be added as a child
func can_add_child_node(node: BaseGraphNode) -> bool:
# Can't add entry, exit, or other parallel groups as children
return node.node_type != "entry" and node.node_type != "exit" and node.node_type != "parallel"

View File

@@ -0,0 +1,63 @@
@tool
class_name TurnActionNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for TurnAction
func _init() -> void:
node_type = "turn"
node_id = "turn_" + str(randi())
title = "Turn"
modulate = Color(0.5, 1.0, 0.5) # Green
# One input and one output connection
var slot = 0
set_slot(slot, true, 0, Color(0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Initialize default parameters
action_parameters["character"] = ""
action_parameters["target"] = ""
action_parameters["turn_speed"] = 2.0
func _setup_parameter_fields() -> void:
# Character field
var char_label = Label.new()
char_label.text = "Character:"
add_child(char_label)
var char_field = LineEdit.new()
char_field.text = action_parameters["character"]
char_field.connect("text_changed", _on_character_changed)
add_child(char_field)
# Target field
var target_label = Label.new()
target_label.text = "Target:"
add_child(target_label)
var target_field = LineEdit.new()
target_field.text = action_parameters["target"]
target_field.connect("text_changed", _on_target_changed)
add_child(target_field)
# Turn speed field
var speed_label = Label.new()
speed_label.text = "Turn Speed:"
add_child(speed_label)
var speed_field = LineEdit.new()
speed_field.text = str(action_parameters["turn_speed"])
speed_field.connect("text_changed", _on_turn_speed_changed)
add_child(speed_field)
func _on_character_changed(new_text: String) -> void:
set_parameter("character", new_text)
func _on_target_changed(new_text: String) -> void:
set_parameter("target", new_text)
func _on_turn_speed_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 2.0
set_parameter("turn_speed", value)

View File

@@ -0,0 +1,35 @@
@tool
class_name WaitActionNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for WaitAction
func _init() -> void:
node_type = "wait"
node_id = "wait_" + str(randi())
title = "Wait"
modulate = Color(0.7, 0.7, 0.7) # Gray
# One input and one output connection
var slot = 0
set_slot(slot, true, 0, Color(0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Initialize default parameters
action_parameters["duration"] = 1.0
func _setup_parameter_fields() -> void:
# Duration field
var duration_label = Label.new()
duration_label.text = "Duration:"
add_child(duration_label)
var duration_field = LineEdit.new()
duration_field.text = str(action_parameters["duration"])
duration_field.connect("text_changed", _on_duration_changed)
add_child(duration_field)
func _on_duration_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 1.0
set_parameter("duration", value)

View File

@@ -0,0 +1,121 @@
@tool
class_name CutsceneResource
extends Resource
# Resource for storing cutscene data
# Properties
@export var nodes: Array = [] # List of node data
@export var connections: Array = [] # List of connection data
@export var parallel_connections: Array = [] # Logical connections for parallel groups
@export var metadata: Dictionary = {} # Additional metadata
# Initialize the resource
func _init() -> void:
nodes = []
connections = []
parallel_connections = []
metadata = {
"version": "1.0",
"created": Time.get_unix_time_from_system(),
"modified": Time.get_unix_time_from_system()
}
# Add a node to the cutscene
func add_node(node_data: Dictionary) -> void:
nodes.append(node_data)
metadata["modified"] = Time.get_unix_time_from_system()
# Remove a node from the cutscene
func remove_node(node_name: String) -> void:
# Remove the node
for i in range(nodes.size()):
if nodes[i].has("name") and nodes[i]["name"] == node_name:
nodes.remove_at(i)
break
# Remove any connections to/from this node
var i = 0
while i < connections.size():
var conn = connections[i]
if conn["from_node"] == node_name or conn["to_node"] == node_name:
connections.remove_at(i)
else:
i += 1
# Remove any parallel connections to/from this node
i = 0
while i < parallel_connections.size():
var conn = parallel_connections[i]
if conn["from_node"] == node_name or conn["to_node"] == node_name:
parallel_connections.remove_at(i)
else:
i += 1
metadata["modified"] = Time.get_unix_time_from_system()
# Add a connection to the cutscene
func add_connection(connection_data: Dictionary) -> void:
connections.append(connection_data)
metadata["modified"] = Time.get_unix_time_from_system()
# Remove a connection from the cutscene
func remove_connection(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
for i in range(connections.size()):
var conn = 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):
connections.remove_at(i)
break
metadata["modified"] = Time.get_unix_time_from_system()
# Add a parallel connection to the cutscene
func add_parallel_connection(connection_data: Dictionary) -> void:
parallel_connections.append(connection_data)
metadata["modified"] = Time.get_unix_time_from_system()
# Remove a parallel connection from the cutscene
func remove_parallel_connection(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
for i in range(parallel_connections.size()):
var conn = parallel_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):
parallel_connections.remove_at(i)
break
metadata["modified"] = Time.get_unix_time_from_system()
# Get node by name
func get_node_by_name(node_name: String) -> Dictionary:
for node in nodes:
if node.has("name") and node["name"] == node_name:
return node
return {}
# Get all connections for a node
func get_connections_for_node(node_name: String) -> Array:
var node_connections = []
for conn in connections:
if conn["from_node"] == node_name or conn["to_node"] == node_name:
node_connections.append(conn)
return node_connections
# Get all parallel connections for a node
func get_parallel_connections_for_node(node_name: String) -> Array:
var node_connections = []
for conn in parallel_connections:
if conn["from_node"] == node_name or conn["to_node"] == node_name:
node_connections.append(conn)
return node_connections
# Clear all data
func clear() -> void:
nodes.clear()
connections.clear()
parallel_connections.clear()
metadata["modified"] = Time.get_unix_time_from_system()
# Update metadata
func update_metadata() -> void:
metadata["modified"] = Time.get_unix_time_from_system()

View File

@@ -0,0 +1,83 @@
@tool
extends Node2D
# Example cutscene using the cutscene editor plugin
# Character nodes
@onready var character1: Node2D = $Character1
@onready var character2: Node2D = $Character2
# Cutscene manager
var cutscene_manager: CutsceneManager
func _ready() -> void:
# Initialize the cutscene system
setup_cutscene()
# Start the cutscene after a short delay to see the initial positions
var start_timer = Timer.new()
start_timer.wait_time = 1.0
start_timer.one_shot = true
start_timer.connect("timeout", start_cutscene)
add_child(start_timer)
start_timer.start()
func setup_cutscene() -> void:
# Create the cutscene manager
cutscene_manager = CutsceneManager.new()
add_child(cutscene_manager)
# Connect to cutscene signals
cutscene_manager.connect("cutscene_started", _on_cutscene_started)
cutscene_manager.connect("cutscene_completed", _on_cutscene_completed)
cutscene_manager.connect("action_started", _on_action_started)
cutscene_manager.connect("action_completed", _on_action_completed)
# Create the action sequence as described in requirements
# 1. & 2. Characters move simultaneously
var parallel_moves = [
MoveAction.new(character1, Vector2(234, 591), 100.0), # Character1 moves
MoveAction.new(character2, Vector2(912, 235), 100.0) # Character2 moves
]
cutscene_manager.add_parallel_actions(parallel_moves)
# 3. Character2 turns to face Character1
var turn_action = TurnAction.new(character2, character1, 1.0)
cutscene_manager.add_action(turn_action)
# 4. Character2 says dialogue
var dialogue_action = DialogueAction.new(character2, "Hello there, friend!", 2.0)
cutscene_manager.add_action(dialogue_action)
# 5. Character1 plays shocked animation (simulated with a wait since we don't have an actual animation)
var animation_action = WaitAction.new(1.0) # Simulate animation with wait
cutscene_manager.add_action(animation_action)
# Add a final dialogue from character1
var final_dialogue = DialogueAction.new(character1, "That was surprising!", 2.0)
cutscene_manager.add_action(final_dialogue)
func start_cutscene() -> void:
print("Starting cutscene...")
cutscene_manager.start()
func _on_cutscene_started() -> void:
print("Cutscene started!")
func _on_cutscene_completed() -> void:
print("Cutscene completed!")
print("Final positions:")
print("Character1: %s" % character1.position)
print("Character2: %s" % character2.position)
func _on_action_started(action: Action) -> void:
print("Action started: %s" % action.name)
func _on_action_completed(action: Action) -> void:
print("Action completed: %s" % action.name)
# Clean up when the node is removed
func _exit_tree() -> void:
if cutscene_manager:
cutscene_manager.queue_free()

View File

@@ -0,0 +1,18 @@
[gd_scene load_steps=2 format=3 uid="uid://example_cutscene"]
[ext_resource type="Script" path="res://addons/cutscene_editor/examples/example_cutscene.gd" id="1"]
[node name="ExampleCutscene" type="Node2D"]
script = ExtResource("1")
[node name="Character1" type="Node2D" parent="."]
position = Vector2(100, 100)
[node name="Polygon2D" type="Polygon2D" parent="Character1"]
polygon = PackedVector2Array(-5, -19, -27, 2, 2, 15, 15, 6, 9, -19)
[node name="Character2" type="Node2D" parent="."]
position = Vector2(200, 100)
[node name="Polygon2D2" type="Polygon2D" parent="Character2"]
polygon = PackedVector2Array(10, -25, -43, 1, -14, 40, 48, 13)

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#cc99cc" stroke="#663366" stroke-width="1"/>
<circle cx="6" cy="8" r="1" fill="#ffffff"/>
<circle cx="10" cy="8" r="1" fill="#ffffff"/>
<path d="M6,10 Q8,12 10,10" fill="none" stroke="#ffffff" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ow1w5ad2w2s"
path="res://.godot/imported/icon_animation.svg-241698a15be9fbca77a14e45efcb07c0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_animation.svg"
dest_files=["res://.godot/imported/icon_animation.svg-241698a15be9fbca77a14e45efcb07c0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#ffff80" stroke="#808000" stroke-width="1"/>
<path d="M4,6 Q8,4 12,6" fill="none" stroke="#000000" stroke-width="1"/>
<path d="M4,10 Q8,12 12,10" fill="none" stroke="#000000" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cwqk0og08i1a6"
path="res://.godot/imported/icon_dialogue.svg-a0f84654cbf69d8bc485290ace9e2d01.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_dialogue.svg"
dest_files=["res://.godot/imported/icon_dialogue.svg-a0f84654cbf69d8bc485290ace9e2d01.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" fill="#80ff80" stroke="#008000" stroke-width="1"/>
<polygon points="6,6 6,10 10,8" fill="#008000"/>
</svg>

After

Width:  |  Height:  |  Size: 222 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dbimchm3l8l45"
path="res://.godot/imported/icon_entry.svg-f83a920a49d4a03c426acaebbf9664c0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_entry.svg"
dest_files=["res://.godot/imported/icon_entry.svg-f83a920a49d4a03c426acaebbf9664c0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" fill="#ff8080" stroke="#800000" stroke-width="1"/>
<line x1="8" y1="4" x2="8" y2="12" stroke="#800000" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://doum444mw5npx"
path="res://.godot/imported/icon_exit.svg-af6dcff1443188587677d8a810d422d2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_exit.svg"
dest_files=["res://.godot/imported/icon_exit.svg-af6dcff1443188587677d8a810d422d2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#6699ff" stroke="#3366cc" stroke-width="1"/>
<line x1="4" y1="8" x2="12" y2="8" stroke="#ffffff" stroke-width="1"/>
<line x1="8" y1="4" x2="8" y2="12" stroke="#ffffff" stroke-width="1"/>
<polygon points="10,6 10,10 14,8" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d32631g3r7so7"
path="res://.godot/imported/icon_move.svg-8960b11003071765119b102b0571d086.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_move.svg"
dest_files=["res://.godot/imported/icon_move.svg-8960b11003071765119b102b0571d086.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#ffcc99" stroke="#cc6600" stroke-width="1"/>
<line x1="4" y1="6" x2="12" y2="6" stroke="#000000" stroke-width="1"/>
<line x1="4" y1="10" x2="12" y2="10" stroke="#000000" stroke-width="1"/>
<line x1="8" y1="6" x2="8" y2="10" stroke="#000000" stroke-width="1" stroke-dasharray="2,2"/>
</svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://buuw640avujpg"
path="res://.godot/imported/icon_parallel.svg-a5bbaaae40552592cadd6e57e5639c21.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_parallel.svg"
dest_files=["res://.godot/imported/icon_parallel.svg-a5bbaaae40552592cadd6e57e5639c21.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#80ff80" stroke="#008000" stroke-width="1"/>
<circle cx="8" cy="8" r="3" fill="none" stroke="#ffffff" stroke-width="1"/>
<line x1="8" y1="8" x2="11" y2="5" stroke="#ffffff" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 335 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dg05cb53hbflj"
path="res://.godot/imported/icon_turn.svg-9e31b9ffad1d239c195dfe0ce34464d9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_turn.svg"
dest_files=["res://.godot/imported/icon_turn.svg-9e31b9ffad1d239c195dfe0ce34464d9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#cccccc" stroke="#666666" stroke-width="1"/>
<circle cx="8" cy="8" r="3" fill="none" stroke="#000000" stroke-width="1"/>
<line x1="8" y1="8" x2="8" y2="5" stroke="#000000" stroke-width="1"/>
<line x1="8" y1="8" x2="10" y2="8" stroke="#000000" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 407 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cy5siww7m3phl"
path="res://.godot/imported/icon_wait.svg-9bc30c71bf95afaa32c61cb9836ac838.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_wait.svg"
dest_files=["res://.godot/imported/icon_wait.svg-9bc30c71bf95afaa32c61cb9836ac838.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,7 @@
[plugin]
name="Cutscene Editor"
description="A visual graph-based editor for designing point-and-click adventure game cutscenes"
author="Adventure AI Team"
version="1.0"
script="CutsceneEditorPlugin.gd"