From 2edf692ce99a4e5bccf37379e12d6107f384b223 Mon Sep 17 00:00:00 2001 From: Bryce Date: Thu, 31 Jul 2025 09:27:26 -0700 Subject: [PATCH] progress. --- action_class_design.md | 116 ---- action_completion_system.md | 275 -------- action_types_design.md | 313 --------- .../cutscene_editor/CutsceneEditorPlugin.gd | 215 ++++++ addons/cutscene_editor/README.md | 59 ++ .../editor/CutsceneGenerator.gd | 312 +++++++++ .../editor/CutsceneGraphEdit.gd | 634 ++++++++++++++++++ .../cutscene_editor/editor/PreviewManager.gd | 274 ++++++++ addons/cutscene_editor/editor/PreviewPanel.gd | 186 +++++ .../cutscene_editor/editor/UndoRedoManager.gd | 246 +++++++ .../editor/nodes/AnimationActionNode.gd | 62 ++ .../editor/nodes/BaseGraphNode.gd | 284 ++++++++ .../editor/nodes/DialogueActionNode.gd | 68 ++ .../cutscene_editor/editor/nodes/EntryNode.gd | 20 + .../cutscene_editor/editor/nodes/ExitNode.gd | 20 + .../editor/nodes/MoveActionNode.gd | 88 +++ .../editor/nodes/ParallelGroupNode.gd | 126 ++++ .../editor/nodes/TurnActionNode.gd | 63 ++ .../editor/nodes/WaitActionNode.gd | 35 + .../editor/resources/CutsceneResource.gd | 121 ++++ .../examples/example_cutscene.gd | 83 +++ .../examples/example_cutscene.tscn | 18 + .../cutscene_editor/icons/icon_animation.svg | 6 + .../icons/icon_animation.svg.import | 37 + .../cutscene_editor/icons/icon_dialogue.svg | 5 + .../icons/icon_dialogue.svg.import | 37 + addons/cutscene_editor/icons/icon_entry.svg | 4 + .../icons/icon_entry.svg.import | 37 + addons/cutscene_editor/icons/icon_exit.svg | 4 + .../icons/icon_exit.svg.import | 37 + addons/cutscene_editor/icons/icon_move.svg | 6 + .../icons/icon_move.svg.import | 37 + .../cutscene_editor/icons/icon_parallel.svg | 6 + .../icons/icon_parallel.svg.import | 37 + addons/cutscene_editor/icons/icon_turn.svg | 5 + .../icons/icon_turn.svg.import | 37 + addons/cutscene_editor/icons/icon_wait.svg | 6 + .../icons/icon_wait.svg.import | 37 + addons/cutscene_editor/plugin.cfg | 7 + cutscene_manager_design.md | 170 ----- cutscene_system_architecture.md | 194 ------ cutscene_system_design.md | 116 ---- cutscene_system_summary.md | 148 ---- extensibility_guide.md | 300 --------- implementation_roadmap.md | 229 ------- project.godot | 4 + sample_cutscene_example.md | 242 ------- 47 files changed, 3263 insertions(+), 2103 deletions(-) delete mode 100644 action_class_design.md delete mode 100644 action_completion_system.md delete mode 100644 action_types_design.md create mode 100644 addons/cutscene_editor/CutsceneEditorPlugin.gd create mode 100644 addons/cutscene_editor/README.md create mode 100644 addons/cutscene_editor/editor/CutsceneGenerator.gd create mode 100644 addons/cutscene_editor/editor/CutsceneGraphEdit.gd create mode 100644 addons/cutscene_editor/editor/PreviewManager.gd create mode 100644 addons/cutscene_editor/editor/PreviewPanel.gd create mode 100644 addons/cutscene_editor/editor/UndoRedoManager.gd create mode 100644 addons/cutscene_editor/editor/nodes/AnimationActionNode.gd create mode 100644 addons/cutscene_editor/editor/nodes/BaseGraphNode.gd create mode 100644 addons/cutscene_editor/editor/nodes/DialogueActionNode.gd create mode 100644 addons/cutscene_editor/editor/nodes/EntryNode.gd create mode 100644 addons/cutscene_editor/editor/nodes/ExitNode.gd create mode 100644 addons/cutscene_editor/editor/nodes/MoveActionNode.gd create mode 100644 addons/cutscene_editor/editor/nodes/ParallelGroupNode.gd create mode 100644 addons/cutscene_editor/editor/nodes/TurnActionNode.gd create mode 100644 addons/cutscene_editor/editor/nodes/WaitActionNode.gd create mode 100644 addons/cutscene_editor/editor/resources/CutsceneResource.gd create mode 100644 addons/cutscene_editor/examples/example_cutscene.gd create mode 100644 addons/cutscene_editor/examples/example_cutscene.tscn create mode 100644 addons/cutscene_editor/icons/icon_animation.svg create mode 100644 addons/cutscene_editor/icons/icon_animation.svg.import create mode 100644 addons/cutscene_editor/icons/icon_dialogue.svg create mode 100644 addons/cutscene_editor/icons/icon_dialogue.svg.import create mode 100644 addons/cutscene_editor/icons/icon_entry.svg create mode 100644 addons/cutscene_editor/icons/icon_entry.svg.import create mode 100644 addons/cutscene_editor/icons/icon_exit.svg create mode 100644 addons/cutscene_editor/icons/icon_exit.svg.import create mode 100644 addons/cutscene_editor/icons/icon_move.svg create mode 100644 addons/cutscene_editor/icons/icon_move.svg.import create mode 100644 addons/cutscene_editor/icons/icon_parallel.svg create mode 100644 addons/cutscene_editor/icons/icon_parallel.svg.import create mode 100644 addons/cutscene_editor/icons/icon_turn.svg create mode 100644 addons/cutscene_editor/icons/icon_turn.svg.import create mode 100644 addons/cutscene_editor/icons/icon_wait.svg create mode 100644 addons/cutscene_editor/icons/icon_wait.svg.import create mode 100644 addons/cutscene_editor/plugin.cfg delete mode 100644 cutscene_manager_design.md delete mode 100644 cutscene_system_architecture.md delete mode 100644 cutscene_system_design.md delete mode 100644 cutscene_system_summary.md delete mode 100644 extensibility_guide.md delete mode 100644 implementation_roadmap.md delete mode 100644 sample_cutscene_example.md diff --git a/action_class_design.md b/action_class_design.md deleted file mode 100644 index 63f2bc4..0000000 --- a/action_class_design.md +++ /dev/null @@ -1,116 +0,0 @@ -# Base Action Class Design - -## Overview -The `Action` class is the abstract base class for all cutscene actions. It defines the common interface and functionality that all actions must implement. - -## Class Structure - -```gdscript -class_name Action -extends RefCounted - -# Action states -enum State { - PENDING, # Action is waiting to be executed - RUNNING, # Action is currently executing - COMPLETED # Action has finished executing -} - -# Public properties -var state: int = State.PENDING -var name: String = "Action" - -# Signals -signal started() # Emitted when action starts -signal completed() # Emitted when action completes -signal failed(error) # Emitted if action fails - -# Virtual methods to be implemented by subclasses -func start() -> void: - # Start executing the action - # This method should be overridden by subclasses - pass - -func update(delta: float) -> void: - # Update the action (called every frame while running) - # This method can be overridden by subclasses - pass - -func is_completed() -> bool: - # Check if the action has completed - # This method should be overridden by subclasses - return state == State.COMPLETED - -func stop() -> void: - # Stop the action (if it's running) - # This method can be overridden by subclasses - pass - -# Helper methods -func _set_running() -> void: - # Set the action state to RUNNING and emit started signal - state = State.RUNNING - started.emit() - -func _set_completed() -> void: - # Set the action state to COMPLETED and emit completed signal - state = State.COMPLETED - completed.emit() - -func _set_failed(error_message: String) -> void: - # Set the action state to FAILED and emit failed signal - state = State.FAILED - failed.emit(error_message) -``` - -## Implementation Guidelines - -### Required Methods -All action subclasses must implement these methods: - -1. `start()` - Initialize and begin the action -2. `is_completed()` - Return true when the action is finished -3. `update(delta)` - Optional, for frame-based updates - -### State Management -Actions should properly manage their state: -- Start in `PENDING` state -- Move to `RUNNING` when `start()` is called -- Move to `COMPLETED` when finished -- Emit appropriate signals for state changes - -### Signal Usage -Actions should emit these signals: -- `started()` when the action begins -- `completed()` when the action finishes successfully -- `failed(error)` when the action encounters an error - -## Example Implementation - -```gdscript -# Example of a simple WaitAction implementation -class_name WaitAction -extends Action - -var duration: float -var elapsed_time: float = 0.0 - -func _init(wait_duration: float): - duration = wait_duration - name = "WaitAction" - -func start() -> void: - ._set_running() - elapsed_time = 0.0 - -func update(delta: float) -> void: - if state == State.RUNNING: - elapsed_time += delta - if elapsed_time >= duration: - ._set_completed() - -func is_completed() -> bool: - return state == State.COMPLETED -``` - -This design provides a solid foundation for all action types while maintaining flexibility for different implementation needs. \ No newline at end of file diff --git a/action_completion_system.md b/action_completion_system.md deleted file mode 100644 index 1905393..0000000 --- a/action_completion_system.md +++ /dev/null @@ -1,275 +0,0 @@ -# Action Completion and Callback System Design - -## Overview -This document details the completion and callback system that allows actions to notify the CutsceneManager when they have finished executing, and enables flexible callback mechanisms for complex action sequences. - -## Signal-Based Completion System - -### Core Signals -Each action implements these core signals: - -```gdscript -# Emitted when the action starts executing -signal started() - -# Emitted when the action completes successfully -signal completed() - -# Emitted when the action fails -signal failed(error_message) - -# Emitted when the action is manually stopped -signal stopped() -``` - -### Signal Connection Flow -1. CutsceneManager creates an action -2. CutsceneManager connects to the action's signals -3. When action starts, it emits `started()` -4. When action completes, it emits `completed()` -5. CutsceneManager's signal handlers update the cutscene state - -```gdscript -# In CutsceneManager -func add_action(action: Action) -> void: - # Connect to action signals - action.connect("started", _on_action_started) - action.connect("completed", _on_action_completed) - action.connect("failed", _on_action_failed) - action.connect("stopped", _on_action_stopped) - - # Add to queue - sequential_actions.append(action) - -func _on_action_started(action: Action) -> void: - emit_signal("action_started", action) - -func _on_action_completed(action: Action) -> void: - # Handle action completion - # Move to next action or complete cutscene - _handle_action_completion(action) - -func _on_action_failed(action: Action, error_message: String) -> void: - # Handle action failure - print("Action failed: %s - %s" % [action.name, error_message]) - # Stop the cutscene or handle error appropriately - stop() - -func _on_action_stopped(action: Action) -> void: - # Handle action being stopped - _handle_action_completion(action) -``` - -## Parallel Action Group Completion - -For parallel actions, the system tracks completion of all actions in a group: - -```gdscript -# Structure for tracking parallel action groups -{ - "actions": [], # Array of actions in this group - "completed_count": 0, # Number of completed actions - "total_count": 0, # Total number of actions - "on_complete": Callable # Optional callback when group completes -} - -func add_parallel_actions(actions: Array, on_complete: Callable = Callable()) -> void: - # Create a parallel action group - var group = { - "actions": actions, - "completed_count": 0, - "total_count": actions.size(), - "on_complete": on_complete - } - - # Connect to each action's completed signal - for action in actions: - action.connect("completed", _on_parallel_action_completed.bind(group, action)) - action.connect("failed", _on_parallel_action_failed.bind(group, action)) - - # Add to active parallel groups - active_parallel_groups.append(group) - - # Add to sequential queue as a single item - sequential_actions.append(group) - -func _on_parallel_action_completed(group: Dictionary, action: Action) -> void: - # Increment completed count - group["completed_count"] += 1 - - # Check if all actions in group are completed - if group["completed_count"] >= group["total_count"]: - # Remove from active groups - active_parallel_groups.erase(group) - - # Call optional completion callback - if group["on_complete"].is_valid(): - group["on_complete"].call() - - # Continue with next sequential action - _execute_next_action() -``` - -## Callback System - -### Action-Level Callbacks -Actions can have callbacks for specific events: - -```gdscript -class_name Action -extends RefCounted - -# Callback properties -var on_started: Callable = Callable() -var on_completed: Callable = Callable() -var on_failed: Callable = Callable() - -func start() -> void: - ._set_running() - - # Call started callback if valid - if on_started.is_valid(): - on_started.call(self) - -func _set_completed() -> void: - state = State.COMPLETED - completed.emit() - - # Call completed callback if valid - if on_completed.is_valid(): - on_completed.call(self) - -func _set_failed(error_message: String) -> void: - state = State.FAILED - failed.emit(error_message) - - # Call failed callback if valid - if on_failed.is_valid(): - on_failed.call(self, error_message) -``` - -### Cutscene-Level Callbacks -The CutsceneManager supports callbacks for cutscene events: - -```gdscript -class_name CutsceneManager -extends Node - -# Cutscene callbacks -var on_started: Callable = Callable() -var on_completed: Callable = Callable() -var on_paused: Callable = Callable() -var on_resumed: Callable = Callable() - -func start() -> void: - if state != State.IDLE: - return - - state = State.RUNNING - is_active = true - emit_signal("cutscene_started") - - # Call started callback - if on_started.is_valid(): - on_started.call() - - # Start first action - _execute_next_action() - -func _on_cutscene_completed() -> void: - state = State.IDLE - is_active = false - emit_signal("cutscene_completed") - - # Call completed callback - if on_completed.is_valid(): - on_completed.call() -``` - -## Chaining Actions with Callbacks - -Actions can be chained together using callbacks: - -```gdscript -# Example: Create a sequence using callbacks -func create_chained_sequence() -> void: - var action1 = MoveAction.new(character1, Vector2(100, 100)) - var action2 = DialogueAction.new(character1, "I have arrived!") - var action3 = AnimationAction.new(character1, "celebrate") - - # Chain actions using callbacks - action1.on_completed = func(action): - _execute_action(action2) - - action2.on_completed = func(action): - _execute_action(action3) - - # Start the sequence - _execute_action(action1) -``` - -## Error Handling and Recovery - -The system includes robust error handling: - -```gdscript -# Action failure handling -func _on_action_failed(action: Action, error_message: String) -> void: - print("Action failed: %s - %s" % [action.name, error_message]) - - # Emit a general failure signal - emit_signal("action_failed", action, error_message) - - # Decide how to handle the failure: - # 1. Stop the entire cutscene - # 2. Skip the failed action and continue - # 3. Retry the action - # 4. Execute an alternative action - - # For now, we'll stop the cutscene - stop() - -# Optional retry mechanism -func _retry_action(action: Action, max_retries: int = 3) -> void: - if not action.has_method("retry_count"): - action.retry_count = 0 - - action.retry_count += 1 - - if action.retry_count <= max_retries: - print("Retrying action: %s (attempt %d)" % [action.name, action.retry_count]) - action.start() - else: - print("Max retries exceeded for action: %s" % action.name) - _on_action_failed(action, "Max retries exceeded") -``` - -## Integration with Godot's Signal System - -The completion system fully integrates with Godot's signal system: - -1. Uses Godot's built-in signal connection mechanisms -2. Supports both method and callable-based connections -3. Handles disconnection automatically when actions are removed -4. Provides type safety through signal parameters - -```gdscript -# Example of connecting to cutscene signals from outside -func setup_cutscene() -> void: - var cutscene = CutsceneManager.new() - - # Connect to cutscene signals - cutscene.connect("cutscene_started", _on_cutscene_started) - cutscene.connect("cutscene_completed", _on_cutscene_completed) - cutscene.connect("action_started", _on_action_started) - cutscene.connect("action_completed", _on_action_completed) - - # Configure callbacks - cutscene.on_completed = func(): - print("Cutscene finished, returning to gameplay") - # Return to gameplay state - - return cutscene -``` - -This completion and callback system provides a flexible, robust foundation for managing action completion in cutscenes while maintaining clean separation of concerns and enabling complex action sequences. \ No newline at end of file diff --git a/action_types_design.md b/action_types_design.md deleted file mode 100644 index a14dc2e..0000000 --- a/action_types_design.md +++ /dev/null @@ -1,313 +0,0 @@ -# Action Types Design - -## Overview -This document details the specific action types that will be implemented for the cutscene system. Each action type handles a specific kind of operation in a cutscene. - -## Core Action Types - -### 1. MoveAction -Moves a character to a specific position over time. - -```gdscript -class_name MoveAction -extends Action - -# Properties -var character: Node2D # The character to move -var target_position: Vector2 # Target position -var speed: float = 100.0 # Movement speed (pixels/second) -var threshold: float = 5.0 # Distance threshold for completion - -# Internal -var start_position: Vector2 -var distance: float -var traveled: float = 0.0 - -func _init(character_node: Node2D, target: Vector2, move_speed: float = 100.0): - character = character_node - target_position = target - speed = move_speed - name = "MoveAction" - -func start() -> void: - if character == null: - ._set_failed("Character is null") - return - - start_position = character.position - distance = start_position.distance_to(target_position) - traveled = 0.0 - ._set_running() - -func update(delta: float) -> void: - if state != State.RUNNING: - return - - if character == null: - ._set_failed("Character was destroyed during action") - return - - # Calculate movement for this frame - var frame_distance = speed * delta - traveled += frame_distance - - # If we've reached or overshot the target - if traveled >= distance: - character.position = target_position - ._set_completed() - return - - # Move character along the path - var direction = (target_position - start_position).normalized() - character.position = start_position + direction * traveled - -func is_completed() -> bool: - return state == State.COMPLETED -``` - -### 2. TurnAction -Makes a character turn to face a direction or another character. - -```gdscript -class_name TurnAction -extends Action - -# Properties -var character: Node2D # The character to turn -var target: Variant # Either a Vector2 position or another Node2D -var turn_speed: float = 2.0 # Rotation speed (radians/second) - -func _init(character_node: Node2D, turn_target: Variant, speed: float = 2.0): - character = character_node - target = turn_target - turn_speed = speed - name = "TurnAction" - -func start() -> void: - if character == null: - ._set_failed("Character is null") - return - ._set_running() - -func update(delta: float) -> void: - if state != State.RUNNING: - return - - if character == null: - ._set_failed("Character was destroyed during action") - return - - # Calculate target rotation - var target_position: Vector2 - if target is Vector2: - target_position = target - elif target is Node2D: - target_position = target.position - else: - ._set_failed("Invalid target type") - return - - var direction = target_position - character.position - var target_rotation = atan2(direction.y, direction.x) - - # Rotate toward target - var angle_diff = angle_difference(character.rotation, target_rotation) - - # If we're close enough, complete the action - if abs(angle_diff) < 0.01: - character.rotation = target_rotation - ._set_completed() - return - - # Rotate based on turn speed - var rotation_amount = sign(angle_diff) * turn_speed * delta - if abs(rotation_amount) > abs(angle_diff): - character.rotation = target_rotation - ._set_completed() - else: - character.rotation += rotation_amount - -func is_completed() -> bool: - return state == State.COMPLETED -``` - -### 3. DialogueAction -Displays dialogue text, potentially with character-specific formatting. - -```gdscript -class_name DialogueAction -extends Action - -# Properties -var character: Node2D # The speaking character (optional) -var text: String # Dialogue text -var duration: float = 0.0 # Duration to display (0 = manual advance) -var auto_advance: bool = true # Whether to auto-advance after duration - -func _init(speaking_character: Node2D, dialogue_text: String, display_duration: float = 0.0): - character = speaking_character - text = dialogue_text - duration = display_duration - name = "DialogueAction" - -func start() -> void: - # In a real implementation, this would interface with a dialogue system - # For now, we'll simulate by printing to console - print("%s: %s" % [character.name if character else "Narrator", text]) - ._set_running() - - # If duration is 0, this is a manual advance action - # In a real game, this would wait for player input - if duration <= 0: - # For demo purposes, we'll complete immediately - # In a real game, this would wait for input - ._set_completed() - else: - # Start a timer for automatic completion - var timer = Timer.new() - timer.wait_time = duration - timer.one_shot = true - timer.connect("timeout", _on_timer_timeout) - # In a real implementation, we'd add this to the scene tree - # add_child(timer) - timer.start() - -func _on_timer_timeout(): - ._set_completed() - -func is_completed() -> bool: - return state == State.COMPLETED -``` - -### 4. AnimationAction -Plays a specific animation on a character. - -```gdscript -class_name AnimationAction -extends Action - -# Properties -var character: Node2D # The character to animate -var animation_name: String # Name of the animation to play -var loop: bool = false # Whether to loop the animation - -func _init(character_node: Node2D, anim_name: String, should_loop: bool = false): - character = character_node - animation_name = anim_name - loop = should_loop - name = "AnimationAction" - -func start() -> void: - if character == null: - ._set_failed("Character is null") - return - - # In a real implementation, this would interface with an AnimationPlayer - # For now, we'll simulate by printing to console - print("Playing animation '%s' on %s" % [animation_name, character.name]) - - # Check if character has an AnimationPlayer - var anim_player = _get_animation_player() - if anim_player == null: - ._set_failed("Character has no AnimationPlayer") - return - - # Connect to animation finished signal for non-looping animations - if not loop: - if not anim_player.is_connected("animation_finished", _on_animation_finished): - anim_player.connect("animation_finished", _on_animation_finished) - - # Play the animation - anim_player.play(animation_name, loop) - ._set_running() - - # For looping animations, we complete immediately - # (they would be stopped by another action) - if loop: - ._set_completed() - -func _get_animation_player() -> AnimationPlayer: - # Try to find AnimationPlayer as a child - for child in character.get_children(): - if child is AnimationPlayer: - return child - - # Try to find AnimationPlayer in the scene tree - return character.get_node_or_null("AnimationPlayer") as AnimationPlayer - -func _on_animation_finished(anim_name: String): - if anim_name == animation_name: - ._set_completed() - -func is_completed() -> bool: - return state == State.COMPLETED -``` - -### 5. WaitAction -Pauses execution for a specified time. - -```gdscript -class_name WaitAction -extends Action - -# Properties -var duration: float # Time to wait in seconds -var elapsed_time: float = 0.0 - -func _init(wait_duration: float): - duration = wait_duration - name = "WaitAction" - -func start() -> void: - elapsed_time = 0.0 - ._set_running() - -func update(delta: float) -> void: - if state != State.RUNNING: - return - - elapsed_time += delta - if elapsed_time >= duration: - ._set_completed() - -func is_completed() -> bool: - return state == State.COMPLETED -``` - -## Extensibility - -To create new action types: - -1. Create a new class that extends `Action` -2. Implement the required methods: - - `start()` - Initialize and begin the action - - `update(delta)` - Update the action each frame (if needed) - - `is_completed()` - Return true when the action is finished -3. Optionally implement `stop()` for cleanup -4. Use appropriate signals to notify completion - -Example of a custom action: - -```gdscript -class_name CustomAction -extends Action - -# Custom properties for this action -var custom_parameter: String - -func _init(param: String): - custom_parameter = param - name = "CustomAction" - -func start() -> void: - # Perform the action - print("Executing custom action with parameter: %s" % custom_parameter) - - # For immediate actions, complete right away - ._set_completed() - -func is_completed() -> bool: - return state == State.COMPLETED -``` - -This design provides a solid set of core action types while maintaining the flexibility to add new types as needed. \ No newline at end of file diff --git a/addons/cutscene_editor/CutsceneEditorPlugin.gd b/addons/cutscene_editor/CutsceneEditorPlugin.gd new file mode 100644 index 0000000..ca34657 --- /dev/null +++ b/addons/cutscene_editor/CutsceneEditorPlugin.gd @@ -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() diff --git a/addons/cutscene_editor/README.md b/addons/cutscene_editor/README.md new file mode 100644 index 0000000..a93920e --- /dev/null +++ b/addons/cutscene_editor/README.md @@ -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) \ No newline at end of file diff --git a/addons/cutscene_editor/editor/CutsceneGenerator.gd b/addons/cutscene_editor/editor/CutsceneGenerator.gd new file mode 100644 index 0000000..f189249 --- /dev/null +++ b/addons/cutscene_editor/editor/CutsceneGenerator.gd @@ -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, " ") diff --git a/addons/cutscene_editor/editor/CutsceneGraphEdit.gd b/addons/cutscene_editor/editor/CutsceneGraphEdit.gd new file mode 100644 index 0000000..0ac1a7e --- /dev/null +++ b/addons/cutscene_editor/editor/CutsceneGraphEdit.gd @@ -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() diff --git a/addons/cutscene_editor/editor/PreviewManager.gd b/addons/cutscene_editor/editor/PreviewManager.gd new file mode 100644 index 0000000..13700bc --- /dev/null +++ b/addons/cutscene_editor/editor/PreviewManager.gd @@ -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 \ No newline at end of file diff --git a/addons/cutscene_editor/editor/PreviewPanel.gd b/addons/cutscene_editor/editor/PreviewPanel.gd new file mode 100644 index 0000000..2cfb9e0 --- /dev/null +++ b/addons/cutscene_editor/editor/PreviewPanel.gd @@ -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 \ No newline at end of file diff --git a/addons/cutscene_editor/editor/UndoRedoManager.gd b/addons/cutscene_editor/editor/UndoRedoManager.gd new file mode 100644 index 0000000..70f7bae --- /dev/null +++ b/addons/cutscene_editor/editor/UndoRedoManager.gd @@ -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 [] \ No newline at end of file diff --git a/addons/cutscene_editor/editor/nodes/AnimationActionNode.gd b/addons/cutscene_editor/editor/nodes/AnimationActionNode.gd new file mode 100644 index 0000000..8b90ac0 --- /dev/null +++ b/addons/cutscene_editor/editor/nodes/AnimationActionNode.gd @@ -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) diff --git a/addons/cutscene_editor/editor/nodes/BaseGraphNode.gd b/addons/cutscene_editor/editor/nodes/BaseGraphNode.gd new file mode 100644 index 0000000..f70d925 --- /dev/null +++ b/addons/cutscene_editor/editor/nodes/BaseGraphNode.gd @@ -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() diff --git a/addons/cutscene_editor/editor/nodes/DialogueActionNode.gd b/addons/cutscene_editor/editor/nodes/DialogueActionNode.gd new file mode 100644 index 0000000..4b0549e --- /dev/null +++ b/addons/cutscene_editor/editor/nodes/DialogueActionNode.gd @@ -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) diff --git a/addons/cutscene_editor/editor/nodes/EntryNode.gd b/addons/cutscene_editor/editor/nodes/EntryNode.gd new file mode 100644 index 0000000..3e73b02 --- /dev/null +++ b/addons/cutscene_editor/editor/nodes/EntryNode.gd @@ -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 diff --git a/addons/cutscene_editor/editor/nodes/ExitNode.gd b/addons/cutscene_editor/editor/nodes/ExitNode.gd new file mode 100644 index 0000000..b31100c --- /dev/null +++ b/addons/cutscene_editor/editor/nodes/ExitNode.gd @@ -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 diff --git a/addons/cutscene_editor/editor/nodes/MoveActionNode.gd b/addons/cutscene_editor/editor/nodes/MoveActionNode.gd new file mode 100644 index 0000000..22418fa --- /dev/null +++ b/addons/cutscene_editor/editor/nodes/MoveActionNode.gd @@ -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) diff --git a/addons/cutscene_editor/editor/nodes/ParallelGroupNode.gd b/addons/cutscene_editor/editor/nodes/ParallelGroupNode.gd new file mode 100644 index 0000000..b781e19 --- /dev/null +++ b/addons/cutscene_editor/editor/nodes/ParallelGroupNode.gd @@ -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" diff --git a/addons/cutscene_editor/editor/nodes/TurnActionNode.gd b/addons/cutscene_editor/editor/nodes/TurnActionNode.gd new file mode 100644 index 0000000..14f7232 --- /dev/null +++ b/addons/cutscene_editor/editor/nodes/TurnActionNode.gd @@ -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) diff --git a/addons/cutscene_editor/editor/nodes/WaitActionNode.gd b/addons/cutscene_editor/editor/nodes/WaitActionNode.gd new file mode 100644 index 0000000..00bad61 --- /dev/null +++ b/addons/cutscene_editor/editor/nodes/WaitActionNode.gd @@ -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) diff --git a/addons/cutscene_editor/editor/resources/CutsceneResource.gd b/addons/cutscene_editor/editor/resources/CutsceneResource.gd new file mode 100644 index 0000000..8486baf --- /dev/null +++ b/addons/cutscene_editor/editor/resources/CutsceneResource.gd @@ -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() diff --git a/addons/cutscene_editor/examples/example_cutscene.gd b/addons/cutscene_editor/examples/example_cutscene.gd new file mode 100644 index 0000000..4dcb515 --- /dev/null +++ b/addons/cutscene_editor/examples/example_cutscene.gd @@ -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() \ No newline at end of file diff --git a/addons/cutscene_editor/examples/example_cutscene.tscn b/addons/cutscene_editor/examples/example_cutscene.tscn new file mode 100644 index 0000000..d38d25f --- /dev/null +++ b/addons/cutscene_editor/examples/example_cutscene.tscn @@ -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) \ No newline at end of file diff --git a/addons/cutscene_editor/icons/icon_animation.svg b/addons/cutscene_editor/icons/icon_animation.svg new file mode 100644 index 0000000..cc4c508 --- /dev/null +++ b/addons/cutscene_editor/icons/icon_animation.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/addons/cutscene_editor/icons/icon_animation.svg.import b/addons/cutscene_editor/icons/icon_animation.svg.import new file mode 100644 index 0000000..4fb52cb --- /dev/null +++ b/addons/cutscene_editor/icons/icon_animation.svg.import @@ -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 diff --git a/addons/cutscene_editor/icons/icon_dialogue.svg b/addons/cutscene_editor/icons/icon_dialogue.svg new file mode 100644 index 0000000..bf3024e --- /dev/null +++ b/addons/cutscene_editor/icons/icon_dialogue.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/addons/cutscene_editor/icons/icon_dialogue.svg.import b/addons/cutscene_editor/icons/icon_dialogue.svg.import new file mode 100644 index 0000000..db25272 --- /dev/null +++ b/addons/cutscene_editor/icons/icon_dialogue.svg.import @@ -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 diff --git a/addons/cutscene_editor/icons/icon_entry.svg b/addons/cutscene_editor/icons/icon_entry.svg new file mode 100644 index 0000000..60c68df --- /dev/null +++ b/addons/cutscene_editor/icons/icon_entry.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/addons/cutscene_editor/icons/icon_entry.svg.import b/addons/cutscene_editor/icons/icon_entry.svg.import new file mode 100644 index 0000000..0ff10de --- /dev/null +++ b/addons/cutscene_editor/icons/icon_entry.svg.import @@ -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 diff --git a/addons/cutscene_editor/icons/icon_exit.svg b/addons/cutscene_editor/icons/icon_exit.svg new file mode 100644 index 0000000..64b478f --- /dev/null +++ b/addons/cutscene_editor/icons/icon_exit.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/addons/cutscene_editor/icons/icon_exit.svg.import b/addons/cutscene_editor/icons/icon_exit.svg.import new file mode 100644 index 0000000..dd09169 --- /dev/null +++ b/addons/cutscene_editor/icons/icon_exit.svg.import @@ -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 diff --git a/addons/cutscene_editor/icons/icon_move.svg b/addons/cutscene_editor/icons/icon_move.svg new file mode 100644 index 0000000..381d056 --- /dev/null +++ b/addons/cutscene_editor/icons/icon_move.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/addons/cutscene_editor/icons/icon_move.svg.import b/addons/cutscene_editor/icons/icon_move.svg.import new file mode 100644 index 0000000..4062037 --- /dev/null +++ b/addons/cutscene_editor/icons/icon_move.svg.import @@ -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 diff --git a/addons/cutscene_editor/icons/icon_parallel.svg b/addons/cutscene_editor/icons/icon_parallel.svg new file mode 100644 index 0000000..30816c7 --- /dev/null +++ b/addons/cutscene_editor/icons/icon_parallel.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/addons/cutscene_editor/icons/icon_parallel.svg.import b/addons/cutscene_editor/icons/icon_parallel.svg.import new file mode 100644 index 0000000..3912f8d --- /dev/null +++ b/addons/cutscene_editor/icons/icon_parallel.svg.import @@ -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 diff --git a/addons/cutscene_editor/icons/icon_turn.svg b/addons/cutscene_editor/icons/icon_turn.svg new file mode 100644 index 0000000..16a856d --- /dev/null +++ b/addons/cutscene_editor/icons/icon_turn.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/addons/cutscene_editor/icons/icon_turn.svg.import b/addons/cutscene_editor/icons/icon_turn.svg.import new file mode 100644 index 0000000..35cf67f --- /dev/null +++ b/addons/cutscene_editor/icons/icon_turn.svg.import @@ -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 diff --git a/addons/cutscene_editor/icons/icon_wait.svg b/addons/cutscene_editor/icons/icon_wait.svg new file mode 100644 index 0000000..d6ce385 --- /dev/null +++ b/addons/cutscene_editor/icons/icon_wait.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/addons/cutscene_editor/icons/icon_wait.svg.import b/addons/cutscene_editor/icons/icon_wait.svg.import new file mode 100644 index 0000000..f579992 --- /dev/null +++ b/addons/cutscene_editor/icons/icon_wait.svg.import @@ -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 diff --git a/addons/cutscene_editor/plugin.cfg b/addons/cutscene_editor/plugin.cfg new file mode 100644 index 0000000..1b91c1b --- /dev/null +++ b/addons/cutscene_editor/plugin.cfg @@ -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" \ No newline at end of file diff --git a/cutscene_manager_design.md b/cutscene_manager_design.md deleted file mode 100644 index 9d0e413..0000000 --- a/cutscene_manager_design.md +++ /dev/null @@ -1,170 +0,0 @@ -# CutsceneManager Design - -## Overview -The `CutsceneManager` is the central orchestrator for executing cutscenes. It manages both sequential and parallel actions, tracks their completion, and controls the flow of the cutscene. - -## Class Structure - -```gdscript -class_name CutsceneManager -extends Node - -# Action queues -var sequential_actions: Array # Actions that run one after another -var current_action: Action # The action currently being executed -var action_index: int = 0 # Index of the next sequential action - -# Parallel actions -var parallel_actions: Array # Groups of actions running simultaneously -var active_parallel_groups: Array # Currently running parallel action groups - -# State management -enum State { - IDLE, # Not running any cutscene - RUNNING, # Currently executing a cutscene - PAUSED # Cutscene is paused -} - -var state: int = State.IDLE -var is_active: bool = false - -# Signals -signal cutscene_started() -signal cutscene_completed() -signal cutscene_paused() -signal cutscene_resumed() -signal action_started(action) -signal action_completed(action) - -# Public methods -func start() -> void: - # Start executing the cutscene - pass - -func pause() -> void: - # Pause the current cutscene - pass - -func resume() -> void: - # Resume a paused cutscene - pass - -func stop() -> void: - # Stop the current cutscene and reset - pass - -func add_action(action: Action) -> void: - # Add a single action to the sequential queue - pass - -func add_parallel_actions(actions: Array) -> void: - # Add a group of actions to run in parallel - pass - -# Internal methods -func _process(delta: float) -> void: - # Main update loop - pass - -func _execute_next_action() -> void: - # Start executing the next sequential action - pass - -func _check_parallel_groups() -> void: - # Check if any parallel action groups have completed - pass - -func _on_action_completed(action: Action) -> void: - # Handle action completion - pass - -func _reset() -> void: - # Reset the manager to initial state - pass -``` - -## Sequential Execution System - -The sequential execution system runs actions one after another: - -1. Actions are added to the `sequential_actions` queue -2. When the cutscene starts, the first action is executed -3. When an action completes, the next one in the queue is started -4. The cutscene completes when all sequential actions are finished - -```gdscript -# Example flow: -# 1. Add actions: [MoveAction, DialogueAction, AnimationAction] -# 2. Start cutscene -# 3. MoveAction starts and runs -# 4. When MoveAction completes, DialogueAction starts -# 5. When DialogueAction completes, AnimationAction starts -# 6. When AnimationAction completes, cutscene ends -``` - -## Parallel Execution System - -The parallel execution system runs multiple actions simultaneously: - -1. Groups of actions are added as parallel groups -2. All actions in a group start at the same time -3. The manager waits for all actions in the group to complete -4. When all actions in a group complete, the next sequential action can proceed - -```gdscript -# Example flow: -# 1. Add sequential action: MoveAction (character1) -# 2. Add parallel group: [MoveAction (character2), MoveAction (character3)] -# 3. Add sequential action: DialogueAction -# -# Execution: -# 1. MoveAction (character1) runs and completes -# 2. Both MoveAction (character2) and MoveAction (character3) start simultaneously -# 3. When both complete, DialogueAction starts -# 4. When DialogueAction completes, cutscene ends -``` - -## Action Group Management - -Parallel actions are managed in groups: - -```gdscript -# Structure for parallel action groups: -{ - "actions": [Action, Action, ...], # The actions in this group - "completed_count": 0, # How many actions have completed - "total_count": 0 # Total number of actions in group -} -``` - -## State Transitions - -The CutsceneManager has these states: -- `IDLE`: Not running any cutscene -- `RUNNING`: Executing a cutscene -- `PAUSED`: Cutscene is temporarily paused - -State transitions: -- IDLE → RUNNING: When `start()` is called -- RUNNING → PAUSED: When `pause()` is called -- PAUSED → RUNNING: When `resume()` is called -- RUNNING → IDLE: When cutscene completes or `stop()` is called -- PAUSED → IDLE: When `stop()` is called - -## Error Handling - -The manager should handle these error cases: -- Attempting to start a cutscene that's already running -- Adding actions while a cutscene is running -- Actions that fail during execution -- Empty action queues - -## Integration with Godot Engine - -The CutsceneManager should: -- Inherit from `Node` to be added to the scene tree -- Use `_process()` for frame-based updates -- Connect to action signals for completion notifications -- Be easily instantiated and configured in the editor or via code - -This design provides a robust foundation for managing complex cutscene sequences with both sequential and parallel execution patterns. \ No newline at end of file diff --git a/cutscene_system_architecture.md b/cutscene_system_architecture.md deleted file mode 100644 index 1b8c87c..0000000 --- a/cutscene_system_architecture.md +++ /dev/null @@ -1,194 +0,0 @@ -# Cutscene System Architecture - -## Overview -This document provides a visual representation of the cutscene system architecture using Mermaid diagrams. - -## System Components - -```mermaid -graph TD - A[CutsceneManager] --> B[Sequential Actions Queue] - A --> C[Parallel Actions Groups] - A --> D[State Management] - A --> E[Signal System] - - B --> F[Action 1] - B --> G[Action 2] - B --> H[Action 3] - - C --> I[Parallel Group 1] - C --> J[Parallel Group 2] - - I --> K[Action A] - I --> L[Action B] - - J --> M[Action C] - J --> N[Action D] - J --> O[Action E] - - F --> P[Action Base Class] - G --> P - H --> P - K --> P - L --> P - M --> P - N --> P - O --> P - - P --> Q[Start Method] - P --> R[Update Method] - P --> S[Is Completed Method] - P --> T[Stop Method] -``` - -## Action State Flow - -```mermaid -stateDiagram-v2 - [*] --> PENDING - PENDING --> RUNNING: start() - RUNNING --> COMPLETED: _set_completed() - RUNNING --> FAILED: _set_failed() - RUNNING --> STOPPED: stop() - FAILED --> [*] - STOPPED --> [*] - COMPLETED --> [*] -``` - -## CutsceneManager State Flow - -```mermaid -stateDiagram-v2 - [*] --> IDLE - IDLE --> RUNNING: start() - RUNNING --> PAUSED: pause() - PAUSED --> RUNNING: resume() - RUNNING --> IDLE: stop() or completion - PAUSED --> IDLE: stop() -``` - -## Action Execution Flow - -```mermaid -sequenceDiagram - participant CM as CutsceneManager - participant SA as SequentialAction - participant PA as ParallelActionA - participant PB as ParallelActionB - - CM->>SA: start() - SA->>CM: started signal - CM->>SA: update() each frame - SA->>CM: completed signal - - CM->>PA: start() - CM->>PB: start() - PA->>CM: started signal - PB->>CM: started signal - CM->>PA: update() each frame - CM->>PB: update() each frame - PA->>CM: completed signal - Note over CM: Waiting for all parallel actions - PB->>CM: completed signal - Note over CM: All parallel actions completed -``` - -## Class Hierarchy - -```mermaid -classDiagram - class Action { - +State state - +String name - +started() - +completed() - +failed() - +start() - +update() - +is_completed() - +stop() - } - - class MoveAction { - +Node2D character - +Vector2 target_position - +float speed - +start() - +update() - +is_completed() - } - - class TurnAction { - +Node2D character - +Variant target - +float turn_speed - +start() - +update() - +is_completed() - } - - class DialogueAction { - +Node2D character - +String text - +float duration - +start() - +update() - +is_completed() - } - - class AnimationAction { - +Node2D character - +String animation_name - +bool loop - +start() - +update() - +is_completed() - } - - class WaitAction { - +float duration - +float elapsed_time - +start() - +update() - +is_completed() - } - - class CutsceneManager { - +Array sequential_actions - +Array parallel_groups - +State state - +start() - +pause() - +resume() - +stop() - +add_action() - +add_parallel_actions() - +_process() - } - - Action <|-- MoveAction - Action <|-- TurnAction - Action <|-- DialogueAction - Action <|-- AnimationAction - Action <|-- WaitAction - CutsceneManager --> Action -``` - -## Signal Connection Flow - -```mermaid -graph LR - A[CutsceneManager] -- Connects to --> B[Action Signals] - B -- started --> A - B -- completed --> A - B -- failed --> A - B -- stopped --> A - - A -- Manages --> C[Sequential Execution] - A -- Manages --> D[Parallel Execution] - - D -- Tracks Completion --> E[Parallel Group Counter] - E -- Notifies When Complete --> A -``` - -This architecture provides a clear, modular design that supports both sequential and parallel action execution while maintaining extensibility for new action types. \ No newline at end of file diff --git a/cutscene_system_design.md b/cutscene_system_design.md deleted file mode 100644 index bb08470..0000000 --- a/cutscene_system_design.md +++ /dev/null @@ -1,116 +0,0 @@ -# Cutscene System Design for 2D Point-and-Click Adventure Game - -## Overview -This document outlines the design for a flexible cutscene system in Godot 4.3. The system will manage actions that occur in sequence or in parallel, with support for time-based actions like character movement. - -## Core Architecture - -### 1. CutsceneManager -The main orchestrator that manages the execution of cutscene actions. - -```gdscript -# Core responsibilities: -# - Managing the action queue -# - Handling sequential and parallel execution -# - Tracking action completion -# - Providing an interface for starting/stopping cutscenes -``` - -### 2. Base Action Class -An abstract base class that all actions will inherit from. - -```gdscript -# Core responsibilities: -# - Defining common interface for all actions -# - Managing action state (pending, running, completed) -# - Providing start/stop functionality -# - Handling completion callbacks -``` - -### 3. Action Execution System -Two main execution modes: -- **Sequential**: Actions execute one after another -- **Parallel**: Multiple actions execute simultaneously - -## Action Types - -### Core Action Types -1. **MoveAction**: Moves a character to a specific position -2. **DialogueAction**: Displays dialogue text -3. **AnimationAction**: Plays a specific animation -4. **TurnAction**: Makes a character turn to face a direction or another character -5. **WaitAction**: Pauses execution for a specified time - -### Extensible Design -The system will be designed to easily add new action types by inheriting from the base Action class. - -## Implementation Details - -### Action State Management -Each action will have these states: -- `PENDING`: Action is waiting to be executed -- `RUNNING`: Action is currently executing -- `COMPLETED`: Action has finished executing - -### Completion System -Actions will use Godot's signal system to notify when they're completed: -- Each action emits a `completed` signal when finished -- The CutsceneManager listens for these signals to determine when to proceed - -### Parallel Execution -For parallel actions: -- Multiple actions start at the same time -- The CutsceneManager waits for all actions to complete before proceeding -- Uses a counter to track how many parallel actions are still running - -## Example Usage - -The system should support scripts like this: - -```gdscript -# Create a cutscene -var cutscene = CutsceneManager.new() - -# Add sequential actions -cutscene.add_action(MoveAction.new(character1, Vector2(234, 591))) -cutscene.add_action(DialogueAction.new(character1, "Hello there!")) - -# Add parallel actions -var parallel_group = [ - MoveAction.new(character1, Vector2(234, 591)), - MoveAction.new(character2, Vector2(912, 235)) -] -cutscene.add_parallel_actions(parallel_group) - -# Add more sequential actions -cutscene.add_action(TurnAction.new(character2, character1)) -cutscene.add_action(DialogueAction.new(character2, "Nice to meet you!")) -cutscene.add_action(AnimationAction.new(character1, "shocked")) - -# Start the cutscene -cutscene.start() -``` - -## File Structure -``` -/cutscene/ - ├── CutsceneManager.gd - ├── actions/ - │ ├── Action.gd (base class) - │ ├── MoveAction.gd - │ ├── DialogueAction.gd - │ ├── AnimationAction.gd - │ ├── TurnAction.gd - │ └── WaitAction.gd - └── examples/ - └── sample_cutscene.gd -``` - -## Extensibility -To add new action types: -1. Create a new class that inherits from `Action.gd` -2. Implement the required methods (`start()`, `is_completed()`, etc.) -3. Add any specific logic for the action type -4. Use in cutscenes like any other action - -This design provides a solid foundation for a flexible cutscene system that can handle both sequential and parallel actions while being easily extensible for new action types. \ No newline at end of file diff --git a/cutscene_system_summary.md b/cutscene_system_summary.md deleted file mode 100644 index f180618..0000000 --- a/cutscene_system_summary.md +++ /dev/null @@ -1,148 +0,0 @@ -# Cutscene System Summary - -## Overview -This document provides a comprehensive summary of the cutscene system designed for a 2D point-and-click adventure game in Godot 4.3. The system supports both sequential and parallel action execution, with a focus on extensibility and dynamic behavior. - -## System Architecture - -### Core Components - -1. **CutsceneManager** - - Central orchestrator for all cutscene actions - - Manages sequential and parallel action execution - - Handles action completion and state transitions - - Provides signals for cutscene events - -2. **Action (Base Class)** - - Abstract base class for all action types - - Defines common interface and state management - - Implements signal-based completion system - - Supports callbacks for flexible chaining - -3. **Specific Action Types** - - MoveAction: Character movement to specific positions - - TurnAction: Character rotation to face targets - - DialogueAction: Text display for conversations - - AnimationAction: Playing character animations - - WaitAction: Time-based delays - -### Key Features - -1. **Sequential Execution** - - Actions execute one after another in order - - Each action must complete before the next begins - - Supports complex linear story sequences - -2. **Parallel Execution** - - Multiple actions can run simultaneously - - System waits for all actions in a group to complete - - Enables synchronized character movements and actions - -3. **Dynamic Behavior** - - Actions are not immediate; they take time to complete - - Frame-based updates for smooth animations - - Asynchronous completion through signal system - -4. **Extensibility** - - Easy to add new action types by extending the base Action class - - Plugin architecture for complex extensions - - Custom event and callback systems - -## Implementation Details - -### File Structure -``` -/cutscene/ - ├── CutsceneManager.gd - ├── Action.gd (base class) - ├── actions/ - │ ├── MoveAction.gd - │ ├── TurnAction.gd - │ ├── DialogueAction.gd - │ ├── AnimationAction.gd - │ └── WaitAction.gd - └── examples/ - └── sample_cutscene.gd -``` - -### Example Usage - -The system supports complex sequences like the one described in the requirements: - -```gdscript -# 1. Character1 moves to 234, 591 -# 2. At the same time Character2 moves to 912, 235 -# 3. When both arrive, character2 turns to character1 -# 4. Character2 says a dialogue line to character1 -# 5. A special animation (shocked) is played for character1 - -var cutscene = CutsceneManager.new() - -# Add parallel movements -var moves = [ - MoveAction.new(character1, Vector2(234, 591)), - MoveAction.new(character2, Vector2(912, 235)) -] -cutscene.add_parallel_actions(moves) - -// Add sequential actions -cutscene.add_action(TurnAction.new(character2, character1)) -cutscene.add_action(DialogueAction.new(character2, "Hello there!")) -cutscene.add_action(AnimationAction.new(character1, "shocked")) - -cutscene.start() -``` - -## Technical Design - -### State Management -- Actions have three states: PENDING, RUNNING, COMPLETED -- CutsceneManager tracks overall state: IDLE, RUNNING, PAUSED -- Proper state transitions with signal notifications - -### Completion System -- Signal-based completion notifications -- Parallel action group tracking -- Error handling and recovery mechanisms -- Callback support for flexible action chaining - -### Performance Considerations -- Object pooling support for frequently created actions -- Efficient signal connection management -- Minimal overhead for inactive actions -- Frame-based updates for smooth animations - -## Extensibility Features - -### Adding New Actions -1. Create a new class extending Action -2. Implement required methods (start, is_completed) -3. Add custom logic in update() if needed -4. Use the action in cutscenes like any other - -### System Extensions -- Plugin architecture for complex features -- Custom event system for dynamic triggers -- Integration points with game state management -- Save/load support for persistent cutscenes - -## Benefits - -1. **Flexibility**: Supports both simple linear sequences and complex parallel actions -2. **Extensibility**: Easy to add new action types and system features -3. **Integration**: Designed to work with existing Godot systems -4. **Performance**: Optimized for smooth gameplay and animations -5. **Maintainability**: Clean separation of concerns and clear interfaces - -## Next Steps - -To implement this system: - -1. Create the base Action class -2. Implement the CutsceneManager -3. Develop the core action types -4. Add the completion and callback system -5. Create sample cutscenes for testing -6. Extend with custom actions as needed - -This design provides a solid foundation for a powerful, flexible cutscene system that can handle the requirements of a 2D point-and-click adventure game while remaining extensible for future needs. \ No newline at end of file diff --git a/extensibility_guide.md b/extensibility_guide.md deleted file mode 100644 index 8ca0840..0000000 --- a/extensibility_guide.md +++ /dev/null @@ -1,300 +0,0 @@ -# Cutscene System Extensibility Guide - -## Overview -This document explains how to extend the cutscene system with new action types, features, and customizations while maintaining compatibility with the existing architecture. - -## Adding New Action Types - -### Basic Extension Process -1. Create a new class that extends `Action` -2. Implement the required methods (`start()`, `is_completed()`) -3. Optionally implement `update()` and `stop()` -4. Add any custom properties or methods needed - -### Example: Custom FadeAction -```gdscript -class_name FadeAction -extends Action - -# Custom properties -var target_node: CanvasItem -var target_alpha: float -var fade_speed: float = 1.0 -var start_alpha: float = 1.0 - -func _init(node: CanvasItem, alpha: float, speed: float = 1.0): - target_node = node - target_alpha = alpha - fade_speed = speed - name = "FadeAction" - -func start() -> void: - if target_node == null: - ._set_failed("Target node is null") - return - - start_alpha = target_node.modulate.a - ._set_running() - -func update(delta: float) -> void: - if state != State.RUNNING: - return - - if target_node == null: - ._set_failed("Target node was destroyed") - return - - # Calculate new alpha value - var current_alpha = target_node.modulate.a - var alpha_diff = target_alpha - current_alpha - var alpha_change = sign(alpha_diff) * fade_speed * delta - - # Check if we've reached the target - if abs(alpha_change) >= abs(alpha_diff): - # Set final alpha and complete - var new_modulate = target_node.modulate - new_modulate.a = target_alpha - target_node.modulate = new_modulate - ._set_completed() - else: - # Apply incremental change - var new_modulate = target_node.modulate - new_modulate.a += alpha_change - target_node.modulate = new_modulate - -func is_completed() -> bool: - return state == State.COMPLETED -``` - -## Customizing the CutsceneManager - -### Adding New Features -The CutsceneManager can be extended with new functionality: - -```gdscript -# Extended CutsceneManager with additional features -class_name ExtendedCutsceneManager -extends CutsceneManager - -# Custom properties -var skip_enabled: bool = true -var auto_skip_delay: float = 0.0 - -# Custom signals -signal cutscene_skipped() - -# Custom methods -func skip_cutscene() -> void: - if not skip_enabled: - return - - emit_signal("cutscene_skipped") - stop() - -func set_auto_skip(seconds: float) -> void: - auto_skip_delay = seconds - # Implementation for auto-skip functionality -``` - -### Plugin Architecture -For complex extensions, consider a plugin-style architecture: - -```gdscript -# Plugin interface -class_name CutscenePlugin -extends RefCounted - -func initialize(manager: CutsceneManager) -> void: - # Initialize plugin with the manager - pass - -func process_action(action: Action, delta: float) -> void: - # Process action each frame - pass - -func cleanup() -> void: - # Clean up when cutscene ends - pass - -# Example plugin for debugging -class_name DebugCutscenePlugin -extends CutscenePlugin - -func process_action(action: Action, delta: float) -> void: - if action.state == Action.State.RUNNING: - print("Action %s is running" % action.name) -``` - -## Integration with External Systems - -### Game State Integration -Connect the cutscene system to your game's state management: - -```gdscript -# Integration with a game state manager -class_name GameStateIntegratedCutsceneManager -extends CutsceneManager - -var game_state_manager: GameStateManager - -func start() -> void: - # Notify game state manager - if game_state_manager: - game_state_manager.set_state(GameState.CUTSCENE) - - # Call parent start method - super().start() - -func _on_cutscene_completed() -> void: - # Notify game state manager - if game_state_manager: - game_state_manager.set_state(GameState.PLAYING) - - # Call parent completion handler - super()._on_cutscene_completed() -``` - -### Save/Load System Integration -Add support for saving and loading cutscene states: - -```gdscript -class_name SaveableCutsceneManager -extends CutsceneManager - -func save_state() -> Dictionary: - return { - "current_action_index": action_index, - "sequential_actions": _serialize_actions(sequential_actions), - "parallel_groups": _serialize_parallel_groups(active_parallel_groups), - "state": state - } - -func load_state(data: Dictionary) -> void: - action_index = data["current_action_index"] - sequential_actions = _deserialize_actions(data["sequential_actions"]) - active_parallel_groups = _deserialize_parallel_groups(data["parallel_groups"]) - state = data["state"] -``` - -## Performance Optimization Extensions - -### Action Pooling -Implement object pooling for frequently created actions: - -```gdscript -class_name PooledCutsceneManager -extends CutsceneManager - -var action_pools: Dictionary = {} - -func get_pooled_action(action_type: String, params: Array) -> Action: - if not action_pools.has(action_type): - action_pools[action_type] = [] - - var pool = action_pools[action_type] - if pool.size() > 0: - # Reuse existing action - var action = pool.pop_back() - # Reinitialize with new parameters - action.reinitialize(params) - return action - else: - # Create new action - return _create_action(action_type, params) - -func return_action_to_pool(action: Action) -> void: - var action_type = action.get_class() - if not action_pools.has(action_type): - action_pools[action_type] = [] - - action.reset() - action_pools[action_type].append(action) -``` - -## Custom Action Composition - -### Action Groups -Create reusable action groups for common sequences: - -```gdscript -class_name ActionGroup -extends Action - -var actions: Array -var current_action_index: int = 0 - -func _init(group_actions: Array): - actions = group_actions - name = "ActionGroup" - -func start() -> void: - current_action_index = 0 - if actions.size() > 0: - _execute_action(actions[0]) - else: - ._set_completed() - -func _execute_action(action: Action) -> void: - action.connect("completed", _on_sub_action_completed.bind(action)) - action.start() - -func _on_sub_action_completed(action: Action) -> void: - current_action_index += 1 - if current_action_index < actions.size(): - _execute_action(actions[current_action_index]) - else: - ._set_completed() -``` - -## Event-Driven Extensions - -### Custom Events -Add support for custom events that can trigger actions: - -```gdscript -class_name EventDrivenCutsceneManager -extends CutsceneManager - -var event_listeners: Dictionary = {} - -func listen_for_event(event_name: String, action: Action) -> void: - if not event_listeners.has(event_name): - event_listeners[event_name] = [] - - event_listeners[event_name].append(action) - -func trigger_event(event_name: String) -> void: - if event_listeners.has(event_name): - for action in event_listeners[event_name]: - # Add action to current sequence or execute immediately - add_action(action) -``` - -## Best Practices for Extensions - -### 1. Maintain Compatibility -- Always call parent methods when overriding -- Keep the same method signatures -- Don't change the core behavior of existing methods - -### 2. Use Composition Over Inheritance -- Prefer adding functionality through plugins or components -- Keep inheritance hierarchies shallow -- Use interfaces where possible - -### 3. Provide Clear Extension Points -- Document which methods are safe to override -- Provide virtual methods for customization -- Use signals for loose coupling - -### 4. Handle Errors Gracefully -- Always check for null references -- Provide meaningful error messages -- Implement fallback behaviors - -### 5. Optimize for Performance -- Reuse objects when possible -- Avoid unnecessary processing -- Profile extensions for performance impact - -This extensibility guide provides a framework for expanding the cutscene system while maintaining its core functionality and ease of use. \ No newline at end of file diff --git a/implementation_roadmap.md b/implementation_roadmap.md deleted file mode 100644 index 375cef4..0000000 --- a/implementation_roadmap.md +++ /dev/null @@ -1,229 +0,0 @@ -# Cutscene System Implementation Roadmap - -## Overview -This document outlines a step-by-step roadmap for implementing the cutscene system in Godot 4.3, based on the architectural designs created. - -## Phase 1: Foundation - -### 1.1 Base Action Class -- Create `Action.gd` base class -- Implement state management (PENDING, RUNNING, COMPLETED) -- Add core signals (started, completed, failed) -- Implement basic methods (start, update, is_completed, stop) - -### 1.2 CutsceneManager Core -- Create `CutsceneManager.gd` -- Implement sequential action queue -- Add action addition methods -- Create basic state management (IDLE, RUNNING) - -## Phase 2: Core Action Types - -### 2.1 WaitAction -- Simplest action to implement and test -- Validates the basic action system -- Tests completion signaling - -### 2.2 MoveAction -- Implement character movement logic -- Add position calculation and frame-based updates -- Test with different speeds and distances - -### 2.3 TurnAction -- Implement character rotation logic -- Handle both position and node targets -- Test smooth rotation animations - -## Phase 3: Advanced Features - -### 3.1 Parallel Execution System -- Implement parallel action group management -- Add completion tracking for parallel actions -- Test with multiple simultaneous actions - -### 3.2 DialogueAction -- Implement basic dialogue display -- Add duration-based completion -- Test with different text lengths - -### 3.3 AnimationAction -- Implement animation playback -- Handle AnimationPlayer integration -- Test with looping and non-looping animations - -## Phase 4: Completion and Callback System - -### 4.1 Signal Integration -- Connect action signals to CutsceneManager -- Implement action completion handling -- Add error handling and failure recovery - -### 4.2 Callback System -- Implement action-level callbacks -- Add cutscene-level callbacks -- Test callback chaining - -### 4.3 State Management -- Implement full state transitions -- Add pause/resume functionality -- Test state persistence - -## Phase 5: Testing and Examples - -### 5.1 Basic Cutscene Example -- Create simple sequential cutscene -- Test all core action types -- Validate completion flow - -### 5.2 Complex Cutscene Example -- Implement the requirement example: - - Character1 moves to 234, 591 - - Character2 moves to 912, 235 (simultaneously) - - Character2 turns to Character1 - - Character2 says dialogue - - Character1 plays shocked animation -- Test parallel execution -- Validate timing and synchronization - -### 5.3 Edge Case Testing -- Test empty action queues -- Test failed actions -- Test rapid state changes -- Test memory management - -## Phase 6: Extensibility Features - -### 6.1 Custom Action Example -- Create a custom action type -- Demonstrate extension process -- Validate compatibility with core system - -### 6.2 Plugin Architecture -- Implement basic plugin system -- Create example plugin -- Test plugin integration - -### 6.3 Performance Optimization -- Implement object pooling -- Add performance monitoring -- Optimize update loops - -## Implementation Order - -```mermaid -graph TD - A[Phase 1: Foundation] --> B[Phase 2: Core Actions] - B --> C[Phase 3: Advanced Features] - C --> D[Phase 4: Completion System] - D --> E[Phase 5: Testing] - E --> F[Phase 6: Extensibility] - - A1[Action Base Class] --> A2[CutsceneManager Core] - A --> A1 - A --> A2 - - B1[WaitAction] --> B2[MoveAction] - B2 --> B3[TurnAction] - B --> B1 - B --> B2 - B --> B3 - - C1[Parallel Execution] --> C2[DialogueAction] - C2 --> C3[AnimationAction] - C --> C1 - C --> C2 - C --> C3 - - D1[Signal Integration] --> D2[Callback System] - D2 --> D3[State Management] - D --> D1 - D --> D2 - D --> D3 - - E1[Basic Example] --> E2[Complex Example] - E2 --> E3[Edge Case Testing] - E --> E1 - E --> E2 - E --> E3 - - F1[Custom Action] --> F2[Plugin Architecture] - F2 --> F3[Performance Optimization] - F --> F1 - F --> F2 - F --> F3 -``` - -## Testing Strategy - -### Unit Tests -- Each action type tested independently -- CutsceneManager state transitions -- Signal emission and connection -- Error handling paths - -### Integration Tests -- Sequential action execution -- Parallel action execution -- Mixed sequential/parallel sequences -- Callback chaining - -### Performance Tests -- Large action queues -- Multiple simultaneous cutscenes -- Memory allocation/garbage collection -- Frame rate impact - -## File Structure Implementation - -``` -res:// -└── cutscene/ - ├── CutsceneManager.gd - ├── Action.gd - ├── actions/ - │ ├── MoveAction.gd - │ ├── TurnAction.gd - │ ├── DialogueAction.gd - │ ├── AnimationAction.gd - │ └── WaitAction.gd - ├── examples/ - │ ├── basic_cutscene.tscn - │ ├── complex_cutscene.tscn - │ └── custom_action_example.gd - └── tests/ - ├── test_action.gd - ├── test_cutscene_manager.gd - └── test_parallel_execution.gd -``` - -## Success Criteria - -### Functional Requirements -- [ ] Sequential actions execute in order -- [ ] Parallel actions execute simultaneously -- [ ] All core action types function correctly -- [ ] Completion system works reliably -- [ ] Error handling is robust - -### Performance Requirements -- [ ] Frame rate remains stable during cutscenes -- [ ] Memory usage is optimized -- [ ] No significant garbage collection spikes - -### Extensibility Requirements -- [ ] New action types can be added easily -- [ ] System can be extended with plugins -- [ ] Custom callbacks work as expected - -## Timeline Estimate - -### Phase 1: Foundation - 2 days -### Phase 2: Core Action Types - 3 days -### Phase 3: Advanced Features - 2 days -### Phase 4: Completion and Callback System - 2 days -### Phase 5: Testing and Examples - 3 days -### Phase 6: Extensibility Features - 2 days - -**Total Estimated Time: 14 days** - -This roadmap provides a structured approach to implementing the cutscene system, ensuring that each component is properly tested and integrated before moving on to the next phase. \ No newline at end of file diff --git a/project.godot b/project.godot index ce71489..8f2650c 100644 --- a/project.godot +++ b/project.godot @@ -14,3 +14,7 @@ config/name="adventure-ai" run/main_scene="res://cutscene/examples/sample_cutscene.tscn" config/features=PackedStringArray("4.3", "Forward Plus") config/icon="res://icon.svg" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/cutscene_editor/plugin.cfg") diff --git a/sample_cutscene_example.md b/sample_cutscene_example.md deleted file mode 100644 index dba379c..0000000 --- a/sample_cutscene_example.md +++ /dev/null @@ -1,242 +0,0 @@ -# Sample Cutscene Example - -## Overview -This document provides a complete example of how to use the cutscene system to create a complex sequence as described in the requirements. - -## Example Scenario -The example implements this sequence: -1. Character1 moves to position (234, 591) -2. At the same time, Character2 moves to position (912, 235) -3. When both arrive, Character2 turns to face Character1 -4. Character2 says a dialogue line to Character1 -5. A special animation (shocked) is played for Character1 - -## Sample Implementation - -```gdscript -# Sample cutscene implementation -extends Node2D - -# Character nodes (would be set in the editor or loaded) -var character1: Node2D -var character2: Node2D - -# Cutscene manager -var cutscene_manager: CutsceneManager - -func _ready() -> void: - # Initialize the cutscene system - setup_cutscene() - - # Start the cutscene - cutscene_manager.start() - -func setup_cutscene() -> void: - # Create the cutscene manager - cutscene_manager = CutsceneManager.new() - add_child(cutscene_manager) - - # Create the action sequence as described in requirements - - # 1. & 2. Characters move simultaneously - var parallel_moves = [ - MoveAction.new(character1, Vector2(234, 591), 150.0), # Character1 moves - MoveAction.new(character2, Vector2(912, 235), 150.0) # Character2 moves - ] - cutscene_manager.add_parallel_actions(parallel_moves) - - # 3. Character2 turns to face Character1 - var turn_action = TurnAction.new(character2, character1, 3.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 - var animation_action = AnimationAction.new(character1, "shocked", false) - cutscene_manager.add_action(animation_action) - - # Set up cutscene callbacks - cutscene_manager.on_completed = func(): - print("Cutscene completed!") - # Return to gameplay or next scene - -# Alternative implementation with more complex chaining -func setup_advanced_cutscene() -> void: - cutscene_manager = CutsceneManager.new() - add_child(cutscene_manager) - - # Create actions - var move1 = MoveAction.new(character1, Vector2(234, 591), 150.0) - var move2 = MoveAction.new(character2, Vector2(912, 235), 150.0) - var turn = TurnAction.new(character2, character1, 3.0) - var dialogue = DialogueAction.new(character2, "Hello there, friend!", 2.0) - var animation = AnimationAction.new(character1, "shocked", false) - - # Add parallel actions - cutscene_manager.add_parallel_actions([move1, move2]) - - # Add sequential actions - cutscene_manager.add_action(turn) - cutscene_manager.add_action(dialogue) - cutscene_manager.add_action(animation) - - # Set completion callback - cutscene_manager.on_completed = _on_cutscene_finished -} - -func _on_cutscene_finished() -> void: - print("The cutscene has finished playing") - # Handle post-cutscene logic here - # For example, transition to gameplay state -``` - -## More Complex Example with Conditional Logic - -```gdscript -# Advanced example with conditional actions -func setup_conditional_cutscene() -> void: - cutscene_manager = CutsceneManager.new() - add_child(cutscene_manager) - - # Move characters into position - var move_group = [ - MoveAction.new(character1, Vector2(300, 400)), - MoveAction.new(character2, Vector2(500, 400)) - ] - cutscene_manager.add_parallel_actions(move_group) - - # Conditional dialogue based on game state - var dialogue_action = DialogueAction.new( - character1, - get_dialogue_text(), - 3.0 - ) - cutscene_manager.add_action(dialogue_action) - - # Play different animation based on dialogue response - var animation_action = AnimationAction.new( - character2, - get_reaction_animation(), - false - ) - cutscene_manager.add_action(animation_action) - - # Wait for a moment - cutscene_manager.add_action(WaitAction.new(1.0)) - - # Final action - var final_dialogue = DialogueAction.new( - character2, - "That was interesting!", - 2.0 - ) - cutscene_manager.add_action(final_dialogue) - -func get_dialogue_text() -> String: - # This could be based on game state, player choices, etc. - return "What do you think about this situation?" - -func get_reaction_animation() -> String: - # This could be based on game state, player choices, etc. - return "thoughtful" -``` - -## Integration with Game Systems - -```gdscript -# Example of integrating with a game state manager -class_name GameCutsceneManager -extends Node - -var cutscene_manager: CutsceneManager -var game_state: GameStateManager - -func play_story_cutscene(story_key: String) -> void: - # Pause gameplay - game_state.set_state(GameState.CUTSCENE) - - # Create cutscene based on story key - var cutscene_data = load_cutscene_data(story_key) - cutscene_manager = create_cutscene_from_data(cutscene_data) - - # Set up completion callback to resume gameplay - cutscene_manager.on_completed = func(): - game_state.set_state(GameState.PLAYING) - # Clean up - cutscene_manager.queue_free() - cutscene_manager = null - - # Start the cutscene - cutscene_manager.start() - -func load_cutscene_data(key: String) -> Dictionary: - # Load cutscene data from a file or database - # This could be JSON, CSV, or custom format - return { - "actions": [ - {"type": "move", "character": "player", "position": [100, 200]}, - {"type": "dialogue", "character": "npc", "text": "Hello!"}, - {"type": "animation", "character": "player", "name": "wave"} - ] - } - -func create_cutscene_from_data(data: Dictionary) -> CutsceneManager: - var manager = CutsceneManager.new() - add_child(manager) - - # Convert data to actions - for action_data in data["actions"]: - var action = create_action_from_data(action_data) - if action: - manager.add_action(action) - - return manager - -func create_action_from_data(data: Dictionary) -> Action: - match data["type"]: - "move": - var character = get_character_by_name(data["character"]) - var position = Vector2(data["position"][0], data["position"][1]) - return MoveAction.new(character, position) - "dialogue": - var character = get_character_by_name(data["character"]) - return DialogueAction.new(character, data["text"]) - "animation": - var character = get_character_by_name(data["character"]) - return AnimationAction.new(character, data["name"]) - _: - return null -``` - -## Performance Considerations - -```gdscript -# Example of optimizing cutscene performance -func setup_optimized_cutscene() -> void: - cutscene_manager = CutsceneManager.new() - add_child(cutscene_manager) - - # Pre-load any resources needed for actions - preload_animations() - preload_dialogue_textures() - - # Use object pooling for frequently created actions - var move_action_pool = [] - for i in range(10): - move_action_pool.append(MoveAction.new(null, Vector2.ZERO)) - - # Reuse actions when possible - var action1 = get_pooled_move_action(character1, Vector2(100, 100)) - var action2 = get_pooled_move_action(character2, Vector2(200, 200)) - - cutscene_manager.add_parallel_actions([action1, action2]) - - # Clean up when done - cutscene_manager.on_completed = func(): - release_pooled_actions() - # Other cleanup -``` - -This sample demonstrates how to use the cutscene system to create complex, dynamic sequences that can handle both sequential and parallel actions while maintaining clean, readable code. \ No newline at end of file