diff --git a/.gitignore b/.gitignore index 0a8c359..e827ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,6 @@ coverage/ # Backup files *.bak *.backup + +# CMake compilation database +compile_commands.json diff --git a/addons/goethe_dialog/CMakeLists.txt b/addons/goethe_dialog/CMakeLists.txt new file mode 100644 index 0000000..fcfce96 --- /dev/null +++ b/addons/goethe_dialog/CMakeLists.txt @@ -0,0 +1,75 @@ + +cmake_minimum_required(VERSION 3.20) + +project(goethe_dialog_extension) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find yaml-cpp (required) +find_package(yaml-cpp REQUIRED) + +# Find zstd (optional) +find_package(PkgConfig QUIET) +if(PkgConfig_FOUND) + pkg_check_modules(ZSTD QUIET libzstd) +endif() + +if(ZSTD_FOUND) + message(STATUS "Found zstd: ${ZSTD_VERSION}") + add_compile_definitions(GOETHE_ZSTD_AVAILABLE) +else() + message(STATUS "zstd not found - compression will use null backend only") +endif() + +# Include directories +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/../../include + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/engine +) + +# Source files from the main library +set(LIBRARY_SOURCES + ../../src/engine/core/dialog.cpp + ../../src/engine/core/compression/backend.cpp + ../../src/engine/core/compression/factory.cpp + ../../src/engine/core/compression/manager.cpp + ../../src/engine/core/compression/register_backends.cpp + ../../src/engine/core/compression/implementations/null.cpp + ../../src/engine/core/statistics.cpp +) + +# Add zstd implementation if available +if(ZSTD_FOUND) + list(APPEND LIBRARY_SOURCES ../../src/engine/core/compression/implementations/zstd.cpp) +endif() + +# Create a static library +add_library(goethe_lib STATIC ${LIBRARY_SOURCES}) + +# Link libraries +target_link_libraries(goethe_lib + PRIVATE + yaml-cpp +) + +if(ZSTD_FOUND) + target_link_libraries(goethe_lib PRIVATE ${ZSTD_LIBRARIES}) + target_include_directories(goethe_lib PRIVATE ${ZSTD_INCLUDE_DIRS}) +endif() + +# Compiler-specific settings +if(MSVC) + target_compile_definitions(goethe_lib PRIVATE _CRT_SECURE_NO_WARNINGS) +else() + target_compile_options(goethe_lib PRIVATE -fvisibility=hidden) +endif() + +# Create a simple test program +add_executable(goethe_test test_main.cpp) +target_link_libraries(goethe_test goethe_lib) + +message(STATUS "Building basic Goethe library and test executable") +message(STATUS "Godot extension will be added once godot-cpp is properly set up") diff --git a/addons/goethe_dialog/README.md b/addons/goethe_dialog/README.md new file mode 100644 index 0000000..47b4714 --- /dev/null +++ b/addons/goethe_dialog/README.md @@ -0,0 +1,185 @@ +# Goethe Dialog Plugin for Godot + +A Godot plugin that integrates the Goethe Dialog System library to provide powerful dialog management capabilities for visual novels and interactive narratives. + +## Features + +- **YAML Dialog Support**: Load and save dialogs in both simple and advanced GOETHE formats +- **Character Management**: Support for character names, expressions, moods, portraits, and voice +- **Conditional Logic**: Advanced condition system with flags, variables, and quest states +- **Effect System**: Comprehensive effect system for game state changes +- **Compression**: Built-in compression support for efficient dialog storage +- **Statistics**: Real-time performance monitoring and analysis +- **Editor Tools**: Custom dialog editor with visual interface +- **GDScript Integration**: Native GDScript classes for easy integration + +## Installation + +1. Copy the `addons/goethe_dialog` folder to your Godot project's `addons` directory +2. Enable the plugin in Project Settings → Plugins +3. Build the extension (see Building section below) + +## Building the Extension + +### Prerequisites + +- Godot 4.0 or later +- CMake 3.20 or later +- C++20 compatible compiler (Clang preferred, GCC fallback) +- yaml-cpp library +- zstd library (optional, for compression) + +### Build Steps + +1. Navigate to the plugin directory: + ```bash + cd addons/goethe_dialog + ``` + +2. Run the build script: + ```bash + chmod +x build.sh + ./build.sh + ``` + +3. The extension will be built and placed in the appropriate `bin` directory. + +## Usage + +### Basic Usage + +```gdscript +# Create a dialog manager +var dialog_manager = GoetheDialogManager.new() +add_child(dialog_manager) + +# Load a dialog from file +dialog_manager.load_dialog_from_file("res://dialogs/chapter1.yaml") + +# Start the dialog +dialog_manager.start_dialog() + +# Connect to signals +dialog_manager.dialog_node_changed.connect(_on_dialog_node_changed) +dialog_manager.choice_made.connect(_on_choice_made) +``` + +### Dialog YAML Format + +#### Simple Format +```yaml +id: chapter1_intro +nodes: + - id: greeting + speaker: alice + text: Hello, welcome to our story! + - id: response + speaker: bob + text: Thank you, I'm excited to begin! +``` + +#### Advanced Format +```yaml +kind: dialogue +id: chapter1_intro +startNode: intro + +nodes: + - id: intro + speaker: marshal + text: Welcome to the adventure! + portrait: { id: marshal, mood: neutral } + voice: { clipId: vo_intro } + choices: + - id: accept + text: I accept the quest + to: quest_start + effects: + - type: SET_FLAG + target: quest_accepted + value: true + - id: refuse + text: I need time to think + to: farewell +``` + +### Character Management + +```gdscript +# Create a character +var character = GoetheCharacter.new() +character.id = "alice" +character.name = "Alice" +character.add_portrait("happy", "neutral", "res://portraits/alice_happy.png") +character.add_voice_clip("greeting", "res://audio/alice_greeting.ogg") +``` + +### Condition System + +```gdscript +# Set flags and variables +dialog_manager.set_flag("quest_completed", true) +dialog_manager.set_variable("player_level", 10) + +# Conditions in YAML will automatically check these values +``` + +## Editor Tools + +### Dialog Editor + +The plugin includes a visual dialog editor that can be accessed from the Tools menu: + +1. Go to Tools → Goethe Dialog Editor +2. Create new dialogs or load existing ones +3. Edit dialog nodes visually +4. Save your changes + +### Custom Types + +The plugin registers several custom types: + +- **GoetheDialogManager**: Main dialog management node +- **GoetheDialogNode**: Individual dialog node resource +- **GoetheCharacter**: Character definition resource + +## Examples + +See the `examples` directory for complete examples: + +- **Basic Dialog**: Simple dialog system implementation +- **Visual Novel**: Full visual novel example with characters and choices + +## API Reference + +### GoetheDialogManager + +#### Properties +- `current_dialog: GoetheDialog` - The currently loaded dialog +- `current_node_index: int` - Index of the current node +- `flags: Dictionary` - Global flags for conditions +- `variables: Dictionary` - Global variables for conditions + +#### Methods +- `load_dialog_from_file(file_path: String) -> bool` +- `load_dialog_from_yaml(yaml_content: String) -> bool` +- `start_dialog(dialog_id: String = "") -> bool` +- `next_node() -> bool` +- `previous_node() -> bool` +- `make_choice(choice_index: int) -> bool` +- `set_flag(flag_name: String, value: bool) -> void` +- `get_flag(flag_name: String) -> bool` +- `set_variable(var_name: String, value: Variant) -> void` +- `get_variable(var_name: String) -> Variant` + +#### Signals +- `dialog_started(dialog_id: String)` +- `dialog_ended(dialog_id: String)` +- `dialog_node_changed(node: GoetheDialogNode)` +- `choice_made(choice_id: String, choice_text: String)` + +### GoetheDialogNode + +#### Properties +- `id: String` - Unique identifier for the node +- `speaker: String` - Charact diff --git a/addons/goethe_dialog/build.sh b/addons/goethe_dialog/build.sh new file mode 100755 index 0000000..a122e46 --- /dev/null +++ b/addons/goethe_dialog/build.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Build script for Goethe Dialog Godot Extension + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}Building Goethe Dialog Extension...${NC}" + +# Check if we're in the right directory +if [ ! -f "CMakeLists.txt" ]; then + echo -e "${RED}Error: CMakeLists.txt not found. Please run this script from the addons/goethe_dialog directory.${NC}" + exit 1 +fi + +# Check if we're in the right project structure +if [ ! -d "../../src/engine" ]; then + echo -e "${RED}Error: Cannot find the main Goethe library source. Please ensure this plugin is in the correct location.${NC}" + echo -e "${YELLOW}Expected path: ../../src/engine${NC}" + echo -e "${YELLOW}Current directory: $(pwd)${NC}" + ls -la ../../ + exit 1 +fi + +# Create build directory +mkdir -p build +cd build + +# Configure with CMake +echo -e "${YELLOW}Configuring with CMake...${NC}" +cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo + +# Build +echo -e "${YELLOW}Building...${NC}" +make -j$(nproc) + +echo -e "${GREEN}Build completed successfully!${NC}" +echo -e "${YELLOW}This builds a basic test executable for now.${NC}" +echo -e "${YELLOW}To build the full Godot extension, you need to:${NC}" +echo -e "${YELLOW}1. Install godot-cpp: git clone https://github.com/godotengine/godot-cpp.git ~/.local/share/godot/godot-cpp${NC}" +echo -e "${YELLOW}2. Build godot-cpp: cd ~/.local/share/godot/godot-cpp && scons platform=linux target=template_release${NC}" +echo -e "${YELLOW}3. Update CMakeLists.txt to include Godot bindings${NC}" diff --git a/addons/goethe_dialog/examples/README.md b/addons/goethe_dialog/examples/README.md new file mode 100644 index 0000000..ce1d517 --- /dev/null +++ b/addons/goethe_dialog/examples/README.md @@ -0,0 +1,113 @@ +# Goethe Dialog Plugin Examples + +This directory contains examples demonstrating how to use the Goethe Dialog Plugin in Godot. + +## Examples + +### Basic Dialog (`basic_dialog/`) + +A simple dialog system implementation that demonstrates: +- Basic dialog loading from YAML +- Dialog navigation (next/previous) +- Choice system +- Simple UI integration + +**Files:** +- `basic_dialog_example.gd` - Main example script +- `dialog_ui.gd` - UI controller for dialog display +- `basic_dialog_example.tscn` - Scene file + +**Usage:** +1. Open the scene in Godot +2. Press "Start Dialog" to begin +3. Use "Next" and "Previous" buttons to navigate +4. Make choices when presented + +### Visual Novel (`visual_novel/`) + +A more advanced example showing: +- Character management with portraits +- Background switching +- Music and voice integration +- Branching storylines +- Advanced dialog features + +**Files:** +- `visual_novel_example.gd` - Main visual novel script +- `visual_novel_example.tscn` - Scene file + +**Features:** +- Multiple characters with portraits +- Background music and voice clips +- Story branching based on choices +- Advanced dialog formatting + +## Running the Examples + +1. **Enable the Plugin:** + - Go to Project Settings → Plugins + - Enable "Goethe Dialog" + +2. **Open an Example:** + - Open the `.tscn` file in Godot + - Run the scene (F5) + +3. **Test Features:** + - Try different dialog options + - Test choice systems + - Experiment with character switching + +## Customizing Examples + +### Adding Your Own Dialog + +Create a YAML file with your dialog: + +```yaml +id: my_story +nodes: + - id: start + speaker: protagonist + text: Hello, this is my story! + - id: choice + speaker: npc + text: What would you like to do? + choices: + - id: option1 + text: Go left + - id: option2 + text: Go right +``` + +### Adding Characters + +```gdscript +var character = GoetheCharacter.new() +character.id = "my_character" +character.name = "My Character" +character.add_portrait("happy", "neutral", "res://portraits/happy.png") +character.add_voice_clip("greeting", "res://audio/greeting.ogg") +``` + +### Loading Custom Dialog + +```gdscript +var dialog_manager = GoetheDialogManager.new() +dialog_manager.load_dialog_from_file("res://my_dialog.yaml") +dialog_manager.start_dialog() +``` + +## Tips + +1. **Dialog Format:** Use the YAML format for easy editing +2. **Characters:** Define characters once and reuse them +3. **Choices:** Use the choice system for branching stories +4. **Effects:** Add effects to choices for game state changes +5. **UI:** Customize the UI to match your game's style + +## Troubleshooting + +- **Plugin not found:** Make sure the plugin is enabled in Project Settings +- **Dialog not loading:** Check the YAML syntax and file paths +- **Characters not showing:** Verify portrait file paths exist +- **Choices not working:** Ensure choice IDs match in your dialog structure diff --git a/addons/goethe_dialog/examples/basic_dialog/basic_dialog_example.gd b/addons/goethe_dialog/examples/basic_dialog/basic_dialog_example.gd new file mode 100644 index 0000000..d257734 --- /dev/null +++ b/addons/goethe_dialog/examples/basic_dialog/basic_dialog_example.gd @@ -0,0 +1,73 @@ +extends Node2D + +@onready var dialog_manager = $DialogManager +@onready var dialog_ui = $DialogUI +@onready var start_button = $StartButton + +func _ready(): + # Connect dialog manager signals + dialog_manager.dialog_started.connect(_on_dialog_started) + dialog_manager.dialog_ended.connect(_on_dialog_ended) + dialog_manager.dialog_node_changed.connect(_on_dialog_node_changed) + dialog_manager.choice_made.connect(_on_choice_made) + + # Connect UI signals + start_button.pressed.connect(_on_start_button_pressed) + dialog_ui.next_button.pressed.connect(_on_next_button_pressed) + dialog_ui.previous_button.pressed.connect(_on_previous_button_pressed) + +func _on_start_button_pressed(): + # Load and start a sample dialog + var sample_dialog = """ +id: sample_dialog +nodes: + - id: greeting + speaker: Alice + text: Hello! Welcome to our story. + - id: response + speaker: Bob + text: Thank you! I'm excited to begin this adventure. + - id: choice + speaker: Alice + text: What would you like to do? + choices: + - id: explore + text: Explore the world + - id: talk + text: Talk to people + - id: explore_result + speaker: Narrator + text: You decide to explore the world around you. + - id: talk_result + speaker: Narrator + text: You decide to talk to the people you meet. +""" + + if dialog_manager.load_dialog_from_yaml(sample_dialog): + dialog_manager.start_dialog() + else: + print("Failed to load dialog") + +func _on_dialog_started(dialog_id: String): + print("Dialog started: ", dialog_id) + dialog_ui.visible = true + start_button.visible = false + +func _on_dialog_ended(dialog_id: String): + print("Dialog ended: ", dialog_id) + dialog_ui.visible = false + start_button.visible = true + +func _on_dialog_node_changed(node: GoetheDialogNode): + dialog_ui.display_node(node) + +func _on_choice_made(choice_id: String, choice_text: String): + print("Choice made: ", choice_id, " - ", choice_text) + # Handle choice logic here + dialog_manager.next_node() + +func _on_next_button_pressed(): + dialog_manager.next_node() + +func _on_previous_button_pressed(): + dialog_manager.previous_node() diff --git a/addons/goethe_dialog/examples/basic_dialog/basic_dialog_example.tscn b/addons/goethe_dialog/examples/basic_dialog/basic_dialog_example.tscn new file mode 100644 index 0000000..981a8c0 --- /dev/null +++ b/addons/goethe_dialog/examples/basic_dialog/basic_dialog_example.tscn @@ -0,0 +1,72 @@ +[gd_scene load_steps=4 format=3 uid="uid://bqxvn8yqxqxqx"] + +[ext_resource type="Script" path="res://addons/goethe_dialog/examples/basic_dialog/basic_dialog_example.gd" id="1_0xqxq"] +[ext_resource type="Script" path="res://addons/goethe_dialog/examples/basic_dialog/dialog_ui.gd" id="2_0xqxq"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1"] +bg_color = Color(0.2, 0.2, 0.2, 0.8) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[node name="BasicDialogExample" type="Node2D"] +script = ExtResource("1_0xqxq") + +[node name="DialogManager" type="Node" parent="."] + +[node name="DialogUI" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("2_0xqxq") + +[node name="VBoxContainer" type="VBoxContainer" parent="DialogUI"] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 50.0 +offset_top = -200.0 +offset_right = -50.0 +offset_bottom = -50.0 + +[node name="SpeakerLabel" type="Label" parent="DialogUI/VBoxContainer"] +layout_mode = 2 +text = "Speaker" +font_size = 18 +font_color = Color(1, 1, 0, 1) + +[node name="TextLabel" type="Label" parent="DialogUI/VBoxContainer"] +layout_mode = 2 +text = "Dialog text will appear here..." +font_size = 16 +autowrap_mode = 2 + +[node name="PortraitTexture" type="TextureRect" parent="DialogUI/VBoxContainer"] +layout_mode = 2 +custom_minimum_size = Vector2(0, 100) +stretch_mode = 5 + +[node name="ChoicesContainer" type="VBoxContainer" parent="DialogUI/VBoxContainer"] +layout_mode = 2 + +[node name="ButtonContainer" type="HBoxContainer" parent="DialogUI/VBoxContainer"] +layout_mode = 2 + +[node name="PreviousButton" type="Button" parent="DialogUI/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Previous" + +[node name="NextButton" type="Button" parent="DialogUI/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Next" + +[node name="StartButton" type="Button" parent="."] +offset_left = 50.0 +offset_top = 50.0 +offset_right = 200.0 +offset_bottom = 100.0 +text = "Start Dialog" diff --git a/addons/goethe_dialog/examples/basic_dialog/dialog_ui.gd b/addons/goethe_dialog/examples/basic_dialog/dialog_ui.gd new file mode 100644 index 0000000..b28b04f --- /dev/null +++ b/addons/goethe_dialog/examples/basic_dialog/dialog_ui.gd @@ -0,0 +1,3 @@ + + + diff --git a/addons/goethe_dialog/examples/visual_novel/visual_novel_example.gd b/addons/goethe_dialog/examples/visual_novel/visual_novel_example.gd new file mode 100644 index 0000000..d896778 --- /dev/null +++ b/addons/goethe_dialog/examples/visual_novel/visual_novel_example.gd @@ -0,0 +1,505 @@ +@tool +extends Node2D +class_name VisualNovelExample + +# Visual Novel Example for Goethe Dialog System +# This demonstrates a complete visual novel with: +# - Character management +# - Background changes +# - Music and sound effects +# - Save/load system +# - Multiple dialog paths +# - Condition-based branching + +signal dialog_started +signal dialog_ended +signal choice_made(choice_id: String, choice_text: String) + +@onready var dialog_manager: GoetheDialogManager = $DialogManager +@onready var dialog_ui: Control = $DialogUI +@onready var background_sprite: Sprite2D = $Background +@onready var character_sprites: Node2D = $Characters +@onready var music_player: AudioStreamPlayer = $MusicPlayer +@onready var sfx_player: AudioStreamPlayer = $SFXPlayer +@onready var save_system: Node = $SaveSystem + +# Character sprites +@onready var alice_sprite: Sprite2D = $Characters/Alice +@onready var bob_sprite: Sprite2D = $Characters/Bob +@onready var narrator_sprite: Sprite2D = $Characters/Narrator + +# UI elements +@onready var speaker_label: Label = $DialogUI/Background/VBoxContainer/SpeakerLabel +@onready var text_label: Label = $DialogUI/Background/VBoxContainer/TextLabel +@onready var portrait_texture: TextureRect = $DialogUI/Background/VBoxContainer/PortraitTexture +@onready var choices_container: VBoxContainer = $DialogUI/Background/VBoxContainer/ChoicesContainer +@onready var next_button: Button = $DialogUI/Background/VBoxContainer/ButtonContainer/NextButton +@onready var previous_button: Button = $DialogUI/Background/VBoxContainer/ButtonContainer/PreviousButton +@onready var auto_button: Button = $DialogUI/Background/VBoxContainer/ButtonContainer/AutoButton +@onready var skip_button: Button = $DialogUI/Background/VBoxContainer/ButtonContainer/SkipButton + +# Menu UI +@onready var menu_button: Button = $MenuButton +@onready var menu_panel: Panel = $MenuPanel +@onready var save_button: Button = $MenuPanel/VBoxContainer/SaveButton +@onready var load_button: Button = $MenuPanel/VBoxContainer/LoadButton +@onready var settings_button: Button = $MenuPanel/VBoxContainer/SettingsButton +@onready var quit_button: Button = $MenuPanel/VBoxContainer/QuitButton + +# Settings UI +@onready var settings_panel: Panel = $SettingsPanel +@onready var text_speed_slider: HSlider = $SettingsPanel/VBoxContainer/TextSpeedSlider +@onready var music_volume_slider: HSlider = $SettingsPanel/VBoxContainer/MusicVolumeSlider +@onready var sfx_volume_slider: HSlider = $SettingsPanel/VBoxContainer/SFXVolumeSlider +@onready var auto_speed_slider: HSlider = $SettingsPanel/VBoxContainer/AutoSpeedSlider + +# Game state +var current_chapter: String = "chapter1" +var is_dialog_active: bool = false +var is_auto_mode: bool = false +var is_skip_mode: bool = false +var text_speed: float = 1.0 +var auto_speed: float = 2.0 +var music_volume: float = 0.7 +var sfx_volume: float = 0.8 + +# Character data +var characters: Dictionary = { + "alice": { + "name": "Alice", + "portraits": { + "happy": preload("res://addons/goethe_dialog/examples/visual_novel/assets/alice_happy.png"), + "sad": preload("res://addons/goethe_dialog/examples/visual_novel/assets/alice_sad.png"), + "angry": preload("res://addons/goethe_dialog/examples/visual_novel/assets/alice_angry.png"), + "surprised": preload("res://addons/goethe_dialog/examples/visual_novel/assets/alice_surprised.png") + }, + "voice": preload("res://addons/goethe_dialog/examples/visual_novel/assets/alice_voice.ogg") + }, + "bob": { + "name": "Bob", + "portraits": { + "happy": preload("res://addons/goethe_dialog/examples/visual_novel/assets/bob_happy.png"), + "sad": preload("res://addons/goethe_dialog/examples/visual_novel/assets/bob_sad.png"), + "angry": preload("res://addons/goethe_dialog/examples/visual_novel/assets/bob_angry.png"), + "surprised": preload("res://addons/goethe_dialog/examples/visual_novel/assets/bob_surprised.png") + }, + "voice": preload("res://addons/goethe_dialog/examples/visual_novel/assets/bob_voice.ogg") + }, + "narrator": { + "name": "Narrator", + "portraits": {}, + "voice": null + } +} + +# Background data +var backgrounds: Dictionary = { + "school_hallway": preload("res://addons/goethe_dialog/examples/visual_novel/assets/school_hallway.png"), + "classroom": preload("res://addons/goethe_dialog/examples/visual_novel/assets/classroom.png"), + "park": preload("res://addons/goethe_dialog/examples/visual_novel/assets/park.png"), + "cafe": preload("res://addons/goethe_dialog/examples/visual_novel/assets/cafe.png"), + "library": preload("res://addons/goethe_dialog/examples/visual_novel/assets/library.png") +} + +# Music data +var music_tracks: Dictionary = { + "main_theme": preload("res://addons/goethe_dialog/examples/visual_novel/assets/main_theme.ogg"), + "romantic": preload("res://addons/goethe_dialog/examples/visual_novel/assets/romantic.ogg"), + "tense": preload("res://addons/goethe_dialog/examples/visual_novel/assets/tense.ogg"), + "happy": preload("res://addons/goethe_dialog/examples/visual_novel/assets/happy.ogg"), + "sad": preload("res://addons/goethe_dialog/examples/visual_novel/assets/sad.ogg") +} + +# Sound effects +var sound_effects: Dictionary = { + "page_turn": preload("res://addons/goethe_dialog/examples/visual_novel/assets/page_turn.ogg"), + "bell": preload("res://addons/goethe_dialog/examples/visual_novel/assets/bell.ogg"), + "footsteps": preload("res://addons/goethe_dialog/examples/visual_novel/assets/footsteps.ogg"), + "door_open": preload("res://addons/goethe_dialog/examples/visual_novel/assets/door_open.ogg"), + "door_close": preload("res://addons/goethe_dialog/examples/visual_novel/assets/door_close.ogg") +} + +func _ready(): + # Connect signals + dialog_manager.dialog_started.connect(_on_dialog_started) + dialog_manager.dialog_ended.connect(_on_dialog_ended) + dialog_manager.dialog_node_changed.connect(_on_dialog_node_changed) + dialog_manager.choice_made.connect(_on_choice_made) + dialog_manager.effect_triggered.connect(_on_effect_triggered) + + # Connect UI signals + next_button.pressed.connect(_on_next_button_pressed) + previous_button.pressed.connect(_on_previous_button_pressed) + auto_button.pressed.connect(_on_auto_button_pressed) + skip_button.pressed.connect(_on_skip_button_pressed) + + menu_button.pressed.connect(_on_menu_button_pressed) + save_button.pressed.connect(_on_save_button_pressed) + load_button.pressed.connect(_on_load_button_pressed) + settings_button.pressed.connect(_on_settings_button_pressed) + quit_button.pressed.connect(_on_quit_button_pressed) + + # Connect settings signals + text_speed_slider.value_changed.connect(_on_text_speed_changed) + music_volume_slider.value_changed.connect(_on_music_volume_changed) + sfx_volume_slider.value_changed.connect(_on_sfx_volume_changed) + auto_speed_slider.value_changed.connect(_on_auto_speed_changed) + + # Initialize UI + _initialize_ui() + + # Load initial dialog + _load_chapter(current_chapter) + +func _initialize_ui(): + # Set initial UI state + dialog_ui.visible = false + menu_panel.visible = false + settings_panel.visible = false + + # Set initial settings + text_speed_slider.value = text_speed + music_volume_slider.value = music_volume + sfx_volume_slider.value = sfx_volume + auto_speed_slider.value = auto_speed + + # Set initial volumes + music_player.volume_db = linear_to_db(music_volume) + sfx_player.volume_db = linear_to_db(sfx_volume) + +func _load_chapter(chapter_id: String): + """Load a chapter dialog file""" + var dialog_file = "res://addons/goethe_dialog/examples/visual_novel/dialogs/" + chapter_id + ".yaml" + + if not FileAccess.file_exists(dialog_file): + push_error("Dialog file not found: " + dialog_file) + return + + if dialog_manager.load_dialog_from_file(dialog_file): + print("Loaded chapter: " + chapter_id) + _play_music("main_theme") + _set_background("school_hallway") + else: + push_error("Failed to load chapter: " + chapter_id) + +func _on_dialog_started(dialog_id: String): + """Called when a dialog starts""" + is_dialog_active = true + dialog_ui.visible = true + dialog_started.emit() + print("Dialog started: " + dialog_id) + +func _on_dialog_ended(dialog_id: String): + """Called when a dialog ends""" + is_dialog_active = false + dialog_ui.visible = false + dialog_ended.emit() + print("Dialog ended: " + dialog_id) + + # Check for next chapter + _check_chapter_progression() + +func _on_dialog_node_changed(node: GoetheDialogNode): + """Called when the dialog node changes""" + _update_ui_for_node(node) + _play_character_voice(node.speaker) + _play_sound_effect("page_turn") + +func _on_choice_made(choice_id: String, choice_text: String): + """Called when a choice is made""" + choice_made.emit(choice_id, choice_text) + print("Choice made: " + choice_id + " - " + choice_text) + + # Hide choices after selection + _hide_choices() + +func _on_effect_triggered(effect_type: String, target: String, value: Variant): + """Called when an effect is triggered""" + print("Effect triggered: " + effect_type + " -> " + target + " = " + str(value)) + + # Handle specific effects + match effect_type: + "CHANGE_BACKGROUND": + _set_background(target) + "PLAY_MUSIC": + _play_music(target) + "PLAY_SFX": + _play_sound_effect(target) + "SHOW_CHARACTER": + _show_character(target, value) + "HIDE_CHARACTER": + _hide_character(target) + "CHANGE_CHAPTER": + _change_chapter(target) + +func _update_ui_for_node(node: GoetheDialogNode): + """Update the UI to display the current dialog node""" + # Update speaker name + speaker_label.text = node.speaker if node.speaker else "" + + # Update dialog text + text_label.text = node.text if node.text else "" + + # Update portrait + _update_portrait(node.speaker, node.expression) + + # Update choices + _update_choices(node.choices) + + # Update character sprites + _update_character_sprites(node.speaker, node.mood) + +func _update_portrait(speaker: String, expression: String): + """Update the character portrait""" + if speaker.is_empty() or not characters.has(speaker.to_lower()): + portrait_texture.texture = null + return + + var character = characters[speaker.to_lower()] + if expression.is_empty(): + expression = "happy" + + if character.portraits.has(expression): + portrait_texture.texture = character.portraits[expression] + else: + portrait_texture.texture = null + +func _update_choices(choices: Array): + """Update the choice buttons""" + # Clear existing choices + for child in choices_container.get_children(): + child.queue_free() + + # Add new choices + for i in range(choices.size()): + var choice = choices[i] + var button = Button.new() + button.text = choice.text + button.custom_minimum_size = Vector2(0, 40) + button.pressed.connect(_on_choice_button_pressed.bind(i)) + choices_container.add_child(button) + +func _update_character_sprites(speaker: String, mood: String): + """Update character sprites on screen""" + # Hide all characters first + for child in character_sprites.get_children(): + child.visible = false + + # Show the speaking character + if not speaker.is_empty() and character_sprites.has_node(speaker.capitalize()): + var sprite = character_sprites.get_node(speaker.capitalize()) + sprite.visible = true + + # Update mood/expression if available + if not mood.is_empty() and characters.has(speaker.to_lower()): + var character = characters[speaker.to_lower()] + if character.portraits.has(mood): + sprite.texture = character.portraits[mood] + +func _hide_choices(): + """Hide all choice buttons""" + for child in choices_container.get_children(): + child.queue_free() + +func _set_background(background_id: String): + """Change the background image""" + if backgrounds.has(background_id): + background_sprite.texture = backgrounds[background_id] + print("Background changed to: " + background_id) + +func _play_music(track_id: String): + """Play background music""" + if music_tracks.has(track_id): + music_player.stream = music_tracks[track_id] + music_player.play() + print("Playing music: " + track_id) + +func _play_sound_effect(sfx_id: String): + """Play a sound effect""" + if sound_effects.has(sfx_id): + sfx_player.stream = sound_effects[sfx_id] + sfx_player.play() + print("Playing SFX: " + sfx_id) + +func _play_character_voice(speaker: String): + """Play character voice""" + if speaker.is_empty() or not characters.has(speaker.to_lower()): + return + + var character = characters[speaker.to_lower()] + if character.voice: + sfx_player.stream = character.voice + sfx_player.play() + +func _show_character(character_id: String, position: Vector2 = Vector2.ZERO): + """Show a character sprite""" + if character_sprites.has_node(character_id.capitalize()): + var sprite = character_sprites.get_node(character_id.capitalize()) + sprite.visible = true + if position != Vector2.ZERO: + sprite.position = position + +func _hide_character(character_id: String): + """Hide a character sprite""" + if character_sprites.has_node(character_id.capitalize()): + var sprite = character_sprites.get_node(character_id.capitalize()) + sprite.visible = false + +func _change_chapter(chapter_id: String): + """Change to a different chapter""" + current_chapter = chapter_id + _load_chapter(chapter_id) + +func _check_chapter_progression(): + """Check if we should progress to the next chapter""" + var condition_system = dialog_manager._get_condition_system() + + # Example chapter progression logic + if current_chapter == "chapter1" and condition_system.get_flag("chapter1_completed"): + _change_chapter("chapter2") + elif current_chapter == "chapter2" and condition_system.get_flag("chapter2_completed"): + _change_chapter("chapter3") + +# UI Event Handlers +func _on_next_button_pressed(): + if is_dialog_active: + dialog_manager.next_node() + +func _on_previous_button_pressed(): + if is_dialog_active: + dialog_manager.previous_node() + +func _on_auto_button_pressed(): + is_auto_mode = !is_auto_mode + auto_button.text = "Auto (ON)" if is_auto_mode else "Auto" + + if is_auto_mode: + _start_auto_mode() + else: + _stop_auto_mode() + +func _on_skip_button_pressed(): + is_skip_mode = !is_skip_mode + skip_button.text = "Skip (ON)" if is_skip_mode else "Skip" + + if is_skip_mode: + _start_skip_mode() + else: + _stop_skip_mode() + +func _on_choice_button_pressed(choice_index: int): + dialog_manager.make_choice(choice_index) + +func _on_menu_button_pressed(): + menu_panel.visible = !menu_panel.visible + settings_panel.visible = false + +func _on_save_button_pressed(): + _save_game() + +func _on_load_button_pressed(): + _load_game() + +func _on_settings_button_pressed(): + settings_panel.visible = !settings_panel.visible + menu_panel.visible = false + +func _on_quit_button_pressed(): + get_tree().quit() + +# Settings Event Handlers +func _on_text_speed_changed(value: float): + text_speed = value + +func _on_music_volume_changed(value: float): + music_volume = value + music_player.volume_db = linear_to_db(value) + +func _on_sfx_volume_changed(value: float): + sfx_volume = value + sfx_player.volume_db = linear_to_db(value) + +func _on_auto_speed_changed(value: float): + auto_speed = value + +# Auto and Skip Mode +func _start_auto_mode(): + # Auto mode will automatically advance dialog + pass + +func _stop_auto_mode(): + # Stop auto mode + pass + +func _start_skip_mode(): + # Skip mode will skip through dialog quickly + pass + +func _stop_skip_mode(): + # Stop skip mode + pass + +# Save/Load System +func _save_game(): + """Save the current game state""" + var save_data = { + "current_chapter": current_chapter, + "dialog_manager_state": dialog_manager.save_state(), + "condition_system_state": dialog_manager._get_condition_system().save_state(), + "settings": { + "text_speed": text_speed, + "music_volume": music_volume, + "sfx_volume": sfx_volume, + "auto_speed": auto_speed + } + } + + var save_file = FileAccess.open("user://savegame.save", FileAccess.WRITE) + if save_file: + save_file.store_string(JSON.stringify(save_data)) + save_file.close() + print("Game saved successfully") + +func _load_game(): + """Load a saved game state""" + var save_file = FileAccess.open("user://savegame.save", FileAccess.READ) + if save_file: + var content = save_file.get_as_text() + save_file.close() + + var save_data = JSON.parse_string(content) + if save_data: + current_chapter = save_data.get("current_chapter", "chapter1") + dialog_manager.load_state(save_data.get("dialog_manager_state", {})) + dialog_manager._get_condition_system().load_state(save_data.get("condition_system_state", {})) + + var settings = save_data.get("settings", {}) + text_speed = settings.get("text_speed", 1.0) + music_volume = settings.get("music_volume", 0.7) + sfx_volume = settings.get("sfx_volume", 0.8) + auto_speed = settings.get("auto_speed", 2.0) + + # Update UI + text_speed_slider.value = text_speed + music_volume_slider.value = music_volume + sfx_volume_slider.value = sfx_volume + auto_speed_slider.value = auto_speed + + # Reload chapter + _load_chapter(current_chapter) + print("Game loaded successfully") + +# Input handling +func _input(event): + if event.is_action_pressed("ui_accept") and is_dialog_active: + _on_next_button_pressed() + elif event.is_action_pressed("ui_cancel"): + if menu_panel.visible: + menu_panel.visible = false + elif settings_panel.visible: + settings_panel.visible = false + elif is_dialog_active: + _on_previous_button_pressed() + elif event.is_action_pressed("ui_menu"): + _on_menu_button_pressed() +``` diff --git a/addons/goethe_dialog/examples/visual_novel/visual_novel_example.tscn b/addons/goethe_dialog/examples/visual_novel/visual_novel_example.tscn new file mode 100644 index 0000000..04f20cc --- /dev/null +++ b/addons/goethe_dialog/examples/visual_novel/visual_novel_example.tscn @@ -0,0 +1,198 @@ +[gd_scene load_steps=2 format=3 uid="uid://bqxvn8yqxqxqx"] + +[ext_resource type="Script" path="res://addons/goethe_dialog/examples/visual_novel/visual_novel_example.gd" id="1_0xqxq"] + +[node name="VisualNovelExample" type="Node2D"] +script = ExtResource("1_0xqxq") + +[node name="Background" type="Sprite2D" parent="."] +position = Vector2(576, 324) +scale = Vector2(1.125, 0.675) + +[node name="Characters" type="Node2D" parent="."] + +[node name="Alice" type="Sprite2D" parent="Characters"] +position = Vector2(400, 400) +visible = false + +[node name="Bob" type="Sprite2D" parent="Characters"] +position = Vector2(750, 400) +visible = false + +[node name="Narrator" type="Sprite2D" parent="Characters"] +position = Vector2(576, 324) +visible = false + +[node name="DialogManager" type="Node" parent="."] + +[node name="DialogUI" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +visible = false + +[node name="Background" type="Panel" parent="DialogUI"] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -250.0 + +[node name="VBoxContainer" type="VBoxContainer" parent="DialogUI/Background"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 20.0 +offset_top = 20.0 +offset_right = -20.0 +offset_bottom = -20.0 + +[node name="SpeakerLabel" type="Label" parent="DialogUI/Background/VBoxContainer"] +layout_mode = 2 +text = "Speaker" +font_size = 18 +font_color = Color(1, 1, 0, 1) + +[node name="TextLabel" type="Label" parent="DialogUI/Background/VBoxContainer"] +layout_mode = 2 +text = "Dialog text will appear here..." +font_size = 16 +autowrap_mode = 2 + +[node name="PortraitTexture" type="TextureRect" parent="DialogUI/Background/VBoxContainer"] +layout_mode = 2 +custom_minimum_size = Vector2(0, 100) +stretch_mode = 5 + +[node name="ChoicesContainer" type="VBoxContainer" parent="DialogUI/Background/VBoxContainer"] +layout_mode = 2 + +[node name="ButtonContainer" type="HBoxContainer" parent="DialogUI/Background/VBoxContainer"] +layout_mode = 2 + +[node name="PreviousButton" type="Button" parent="DialogUI/Background/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Previous" + +[node name="NextButton" type="Button" parent="DialogUI/Background/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Next" + +[node name="AutoButton" type="Button" parent="DialogUI/Background/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Auto" + +[node name="SkipButton" type="Button" parent="DialogUI/Background/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Skip" + +[node name="MenuButton" type="Button" parent="."] +offset_left = 50.0 +offset_top = 50.0 +offset_right = 150.0 +offset_bottom = 80.0 +text = "Menu" + +[node name="MenuPanel" type="Panel" parent="."] +offset_left = 400.0 +offset_top = 200.0 +offset_right = 750.0 +offset_bottom = 450.0 +visible = false + +[node name="VBoxContainer" type="VBoxContainer" parent="MenuPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 20.0 +offset_top = 20.0 +offset_right = -20.0 +offset_bottom = -20.0 + +[node name="SaveButton" type="Button" parent="MenuPanel/VBoxContainer"] +layout_mode = 2 +text = "Save Game" + +[node name="LoadButton" type="Button" parent="MenuPanel/VBoxContainer"] +layout_mode = 2 +text = "Load Game" + +[node name="SettingsButton" type="Button" parent="MenuPanel/VBoxContainer"] +layout_mode = 2 +text = "Settings" + +[node name="QuitButton" type="Button" parent="MenuPanel/VBoxContainer"] +layout_mode = 2 +text = "Quit Game" + +[node name="SettingsPanel" type="Panel" parent="."] +offset_left = 400.0 +offset_top = 200.0 +offset_right = 750.0 +offset_bottom = 500.0 +visible = false + +[node name="VBoxContainer" type="VBoxContainer" parent="SettingsPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 20.0 +offset_top = 20.0 +offset_right = -20.0 +offset_bottom = -20.0 + +[node name="Label" type="Label" parent="SettingsPanel/VBoxContainer"] +layout_mode = 2 +text = "Settings" +horizontal_alignment = 1 + +[node name="TextSpeedSlider" type="HSlider" parent="SettingsPanel/VBoxContainer"] +layout_mode = 2 +max_value = 3.0 +value = 1.0 + +[node name="TextSpeedLabel" type="Label" parent="SettingsPanel/VBoxContainer"] +layout_mode = 2 +text = "Text Speed" + +[node name="MusicVolumeSlider" type="HSlider" parent="SettingsPanel/VBoxContainer"] +layout_mode = 2 +max_value = 1.0 +value = 0.7 + +[node name="MusicVolumeLabel" type="Label" parent="SettingsPanel/VBoxContainer"] +layout_mode = 2 +text = "Music Volume" + +[node name="SFXVolumeSlider" type="HSlider" parent="SettingsPanel/VBoxContainer"] +layout_mode = 2 +max_value = 1.0 +value = 0.8 + +[node name="SFXVolumeLabel" type="Label" parent="SettingsPanel/VBoxContainer"] +layout_mode = 2 +text = "SFX Volume" + +[node name="AutoSpeedSlider" type="HSlider" parent="SettingsPanel/VBoxContainer"] +layout_mode = 2 +max_value = 5.0 +value = 2.0 + +[node name="AutoSpeedLabel" type="Label" parent="SettingsPanel/VBoxContainer"] +layout_mode = 2 +text = "Auto Speed" + +[node name="CloseButton" type="Button" parent="SettingsPanel/VBoxContainer"] +layout_mode = 2 +text = "Close" + +[node name="MusicPlayer" type="AudioStreamPlayer" parent="."] + +[node name="SFXPlayer" type="AudioStreamPlayer" parent="."] + +[node name="SaveSystem" type="Node" parent="."] diff --git a/addons/goethe_dialog/goethe_dialog.gdextension b/addons/goethe_dialog/goethe_dialog.gdextension new file mode 100644 index 0000000..b28b04f --- /dev/null +++ b/addons/goethe_dialog/goethe_dialog.gdextension @@ -0,0 +1,3 @@ + + + diff --git a/addons/goethe_dialog/goethe_test b/addons/goethe_dialog/goethe_test new file mode 100755 index 0000000..21d02cb Binary files /dev/null and b/addons/goethe_dialog/goethe_test differ diff --git a/addons/goethe_dialog/plugin.gd b/addons/goethe_dialog/plugin.gd new file mode 100644 index 0000000..aed7331 --- /dev/null +++ b/addons/goethe_dialog/plugin.gd @@ -0,0 +1,32 @@ +@tool +extends EditorPlugin + +const PLUGIN_NAME = "GoetheDialog" + +func _enter_tree(): + # Register custom types + add_custom_type("GoetheDialogManager", "Node", preload("res://addons/goethe_dialog/scripts/goethe_dialog_manager.gd"), preload("res://addons/goethe_dialog/icons/dialog_manager_icon.svg")) + add_custom_type("GoetheDialogNode", "Resource", preload("res://addons/goethe_dialog/scripts/goethe_dialog_node.gd"), preload("res://addons/goethe_dialog/icons/dialog_node_icon.svg")) + add_custom_type("GoetheCharacter", "Resource", preload("res://addons/goethe_dialog/scripts/goethe_character.gd"), preload("res://addons/goethe_dialog/icons/character_icon.svg")) + + # Add editor tools + add_tool_menu_item("Goethe Dialog Editor", _open_dialog_editor) + + print("Goethe Dialog Plugin loaded successfully!") + +func _exit_tree(): + # Remove custom types + remove_custom_type("GoetheDialogManager") + remove_custom_type("GoetheDialogNode") + remove_custom_type("GoetheCharacter") + + # Remove editor tools + remove_tool_menu_item("Goethe Dialog Editor") + + print("Goethe Dialog Plugin unloaded!") + +func _open_dialog_editor(): + # Open the dialog editor window + var editor = preload("res://addons/goethe_dialog/scenes/dialog_editor.tscn").instantiate() + get_editor_interface().get_base_control().add_child(editor) + editor.popup_centered() diff --git a/addons/goethe_dialog/scenes/dialog_editor.gd b/addons/goethe_dialog/scenes/dialog_editor.gd new file mode 100644 index 0000000..9d9f970 --- /dev/null +++ b/addons/goethe_dialog/scenes/dialog_editor.gd @@ -0,0 +1,162 @@ +@tool +extends Window + +@onready var dialog_tree = $VBoxContainer/HSplitContainer/DialogTree +@onready var node_editor = $VBoxContainer/HSplitContainer/NodeEditor +@onready var file_menu = $VBoxContainer/MenuBar/FileMenu + +var current_dialog: GoetheDialog +var current_file_path: String = "" + +func _ready(): + # Set up the window + title = "Goethe Dialog Editor" + size = Vector2(1200, 800) + + # Connect signals + file_menu.get_popup().id_pressed.connect(_on_file_menu_selected) + + # Initialize dialog tree + _setup_dialog_tree() + + # Initialize node editor + _setup_node_editor() + +func _setup_dialog_tree(): + # Set up the dialog tree for displaying dialog structure + dialog_tree.set_columns(3) + dialog_tree.set_column_title(0, "ID") + dialog_tree.set_column_title(1, "Speaker") + dialog_tree.set_column_title(2, "Text") + + dialog_tree.item_selected.connect(_on_dialog_node_selected) + +func _setup_node_editor(): + # Set up the node editor for editing individual nodes + pass + +func _on_file_menu_selected(id: int): + match id: + 0: # New + _new_dialog() + 1: # Open + _open_dialog() + 2: # Save + _save_dialog() + 3: # Save As + _save_dialog_as() + +func _new_dialog(): + current_dialog = GoetheDialog.new() + current_dialog.id = "new_dialog" + current_file_path = "" + _refresh_dialog_tree() + +func _open_dialog(): + var file_dialog = FileDialog.new() + file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE + file_dialog.add_filter("*.yaml", "YAML Files") + file_dialog.add_filter("*.yml", "YAML Files") + + file_dialog.file_selected.connect(_on_file_selected) + add_child(file_dialog) + file_dialog.popup_centered() + +func _on_file_selected(path: String): + current_file_path = path + + var dialog_manager = GoetheDialogManager.new() + if dialog_manager.load_dialog_from_file(path): + current_dialog = dialog_manager.current_dialog + _refresh_dialog_tree() + else: + OS.alert("Failed to load dialog file", "Error") + +func _save_dialog(): + if current_file_path.is_empty(): + _save_dialog_as() + else: + _save_dialog_to_path(current_file_path) + +func _save_dialog_as(): + var file_dialog = FileDialog.new() + file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE + file_dialog.add_filter("*.yaml", "YAML Files") + file_dialog.add_filter("*.yml", "YAML Files") + + file_dialog.file_selected.connect(_on_save_file_selected) + add_child(file_dialog) + file_dialog.popup_centered() + +func _on_save_file_selected(path: String): + current_file_path = path + _save_dialog_to_path(path) + +func _save_dialog_to_path(path: String): + if not current_dialog: + return + + # Convert dialog to YAML and save + var yaml_content = _dialog_to_yaml(current_dialog) + + var file = FileAccess.open(path, FileAccess.WRITE) + if file: + file.store_string(yaml_content) + file.close() + OS.alert("Dialog saved successfully", "Success") + else: + OS.alert("Failed to save dialog file", "Error") + +func _refresh_dialog_tree(): + dialog_tree.clear() + + if not current_dialog: + return + + var root = dialog_tree.create_item() + root.set_text(0, current_dialog.id) + root.set_text(1, "Dialog") + root.set_text(2, str(current_dialog.nodes.size()) + " nodes") + + for i in range(current_dialog.nodes.size()): + var node = current_dialog.nodes[i] + var item = dialog_tree.create_item(root) + item.set_text(0, node.id) + item.set_text(1, node.speaker) + item.set_text(2, node.text.left(50) + "..." if node.text.length() > 50 else node.text) + +func _on_dialog_node_selected(): + var selected_item = dialog_tree.get_selected() + if not selected_item or not current_dialog: + return + + # Find the selected node and show it in the editor + var node_index = selected_item.get_index() + if node_index < current_dialog.nodes.size(): + _show_node_in_editor(current_dialog.nodes[node_index]) + +func _show_node_in_editor(node: GoetheDialogNode): + # Show the node in the node editor + pass + +func _dialog_to_yaml(dialog: GoetheDialog) -> String: + # Convert dialog to YAML string + var yaml = "id: " + dialog.id + "\n" + yaml += "nodes:\n" + + for node in dialog.nodes: + yaml += " - id: " + node.id + "\n" + if not node.speaker.is_empty(): + yaml += " speaker: " + node.speaker + "\n" + if not node.text.is_empty(): + yaml += " text: " + node.text + "\n" + if not node.portrait.is_empty(): + yaml += " portrait: " + node.portrait + "\n" + if not node.voice.is_empty(): + yaml += " voice: " + node.voice + "\n" + + return yaml +``` + +``` + diff --git a/addons/goethe_dialog/scenes/dialog_editor.tscn b/addons/goethe_dialog/scenes/dialog_editor.tscn new file mode 100644 index 0000000..5ad9e62 --- /dev/null +++ b/addons/goethe_dialog/scenes/dialog_editor.tscn @@ -0,0 +1,51 @@ +[gd_scene load_steps=2 format=3 uid="uid://bqxvn8yqxqxqx"] + +[ext_resource type="Script" path="res://addons/goethe_dialog/scenes/dialog_editor.gd" id="1_0xqxq"] + +[node name="DialogEditor" type="Window"] +script = ExtResource("1_0xqxq") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] + +[node name="MenuBar" type="HBoxContainer" parent="VBoxContainer"] + +[node name="FileMenu" type="MenuButton" parent="VBoxContainer/MenuBar"] +text = "File" + +[node name="HSplitContainer" type="HSplitContainer" parent="VBoxContainer"] + +[node name="DialogTree" type="Tree" parent="VBoxContainer/HSplitContainer"] +split_offset = 300 + +[node name="NodeEditor" type="VBoxContainer" parent="VBoxContainer/HSplitContainer"] + +[node name="NodeProperties" type="VBoxContainer" parent="VBoxContainer/HSplitContainer/NodeEditor"] + +[node name="IDLabel" type="Label" parent="VBoxContainer/HSplitContainer/NodeEditor/NodeProperties"] +text = "Node ID:" + +[node name="IDEdit" type="LineEdit" parent="VBoxContainer/HSplitContainer/NodeEditor/NodeProperties"] + +[node name="SpeakerLabel" type="Label" parent="VBoxContainer/HSplitContainer/NodeEditor/NodeProperties"] +text = "Speaker:" + +[node name="SpeakerEdit" type="LineEdit" parent="VBoxContainer/HSplitContainer/NodeEditor/NodeProperties"] + +[node name="TextLabel" type="Label" parent="VBoxContainer/HSplitContainer/NodeEditor/NodeProperties"] +text = "Text:" + +[node name="TextEdit" type="TextEdit" parent="VBoxContainer/HSplitContainer/NodeEditor/NodeProperties"] +custom_minimum_size = Vector2(0, 100) + +[node name="ChoicesLabel" type="Label" parent="VBoxContainer/HSplitContainer/NodeEditor/NodeEditor"] +text = "Choices:" + +[node name="ChoicesList" type="ItemList" parent="VBoxContainer/HSplitContainer/NodeEditor/NodeEditor"] +custom_minimum_size = Vector2(0, 150) + +[node name="AddChoiceButton" type="Button" parent="VBoxContainer/HSplitContainer/NodeEditor/NodeEditor"] +text = "Add Choice" +``` + +``` + diff --git a/addons/goethe_dialog/scenes/dialog_manager.tscn b/addons/goethe_dialog/scenes/dialog_manager.tscn new file mode 100644 index 0000000..8991da0 --- /dev/null +++ b/addons/goethe_dialog/scenes/dialog_manager.tscn @@ -0,0 +1,56 @@ +[gd_scene load_steps=2 format=3 uid="uid://bqxvn8yqxqxqx"] + +[ext_resource type="Script" path="res://addons/goethe_dialog/scripts/goethe_dialog_manager.gd" id="1_0xqxq"] + +[node name="DialogManager" type="Node"] +script = ExtResource("1_0xqxq") + +[node name="ConditionSystem" type="Node" parent="."] + +[node name="DialogUI" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="VBoxContainer" type="VBoxContainer" parent="DialogUI"] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -200.0 + +[node name="SpeakerLabel" type="Label" parent="DialogUI/VBoxContainer"] +layout_mode = 2 +text = "Speaker" +font_size = 18 +font_color = Color(1, 1, 0, 1) + +[node name="TextLabel" type="Label" parent="DialogUI/VBoxContainer"] +layout_mode = 2 +text = "Dialog text will appear here..." +font_size = 16 +autowrap_mode = 2 + +[node name="ChoicesContainer" type="VBoxContainer" parent="DialogUI/VBoxContainer"] +layout_mode = 2 + +[node name="ButtonContainer" type="HBoxContainer" parent="DialogUI/VBoxContainer"] +layout_mode = 2 + +[node name="PreviousButton" type="Button" parent="DialogUI/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Previous" + +[node name="NextButton" type="Button" parent="DialogUI/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Next" + +[node name="AutoButton" type="Button" parent="DialogUI/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Auto" + +[node name="SkipButton" type="Button" parent="DialogUI/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Skip" diff --git a/addons/goethe_dialog/scenes/dialog_ui.tscn b/addons/goethe_dialog/scenes/dialog_ui.tscn new file mode 100644 index 0000000..a20afe9 --- /dev/null +++ b/addons/goethe_dialog/scenes/dialog_ui.tscn @@ -0,0 +1,67 @@ +[gd_scene load_steps=2 format=3 uid="uid://bqxvn8yqxqxqx"] + +[ext_resource type="Script" path="res://addons/goethe_dialog/examples/basic_dialog/dialog_ui.gd" id="1_0xqxq"] + +[node name="DialogUI" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1_0xqxq") + +[node name="Background" type="Panel" parent="."] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -250.0 + +[node name="VBoxContainer" type="VBoxContainer" parent="Background"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 20.0 +offset_top = 20.0 +offset_right = -20.0 +offset_bottom = -20.0 + +[node name="SpeakerLabel" type="Label" parent="Background/VBoxContainer"] +layout_mode = 2 +text = "Speaker" +font_size = 18 +font_color = Color(1, 1, 0, 1) + +[node name="TextLabel" type="Label" parent="Background/VBoxContainer"] +layout_mode = 2 +text = "Dialog text will appear here..." +font_size = 16 +autowrap_mode = 2 + +[node name="PortraitTexture" type="TextureRect" parent="Background/VBoxContainer"] +layout_mode = 2 +custom_minimum_size = Vector2(0, 100) +stretch_mode = 5 + +[node name="ChoicesContainer" type="VBoxContainer" parent="Background/VBoxContainer"] +layout_mode = 2 + +[node name="ButtonContainer" type="HBoxContainer" parent="Background/VBoxContainer"] +layout_mode = 2 + +[node name="PreviousButton" type="Button" parent="Background/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Previous" + +[node name="NextButton" type="Button" parent="Background/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Next" + +[node name="AutoButton" type="Button" parent="Background/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Auto" + +[node name="SkipButton" type="Button" parent="Background/VBoxContainer/ButtonContainer"] +layout_mode = 2 +text = "Skip" diff --git a/addons/goethe_dialog/scripts/condition_system.gd b/addons/goethe_dialog/scripts/condition_system.gd new file mode 100644 index 0000000..446fef4 --- /dev/null +++ b/addons/goethe_dialog/scripts/condition_system.gd @@ -0,0 +1,381 @@ +@tool +extends Node +class_name GoetheConditionSystem + +# Autoload singleton for global condition management +static var instance: GoetheConditionSystem + +# Global flags and variables +var flags: Dictionary = {} +var variables: Dictionary = {} +var quest_states: Dictionary = {} +var chapter_states: Dictionary = {} +var area_states: Dictionary = {} +var dialog_history: Array[String] = [] +var choice_history: Array[String] = [] + +# Events and timers +var events: Dictionary = {} +var timers: Dictionary = {} + +# Inventory system +var inventory: Dictionary = {} + +# Door and access control +var door_states: Dictionary = {} +var access_permissions: Dictionary = {} + +signal flag_changed(flag_name: String, new_value: bool) +signal variable_changed(var_name: String, new_value: Variant) +signal quest_state_changed(quest_id: String, new_state: String) +signal condition_evaluated(condition: Dictionary, result: bool) + +func _ready(): + # Set up as singleton + if instance == null: + instance = self + # Make sure this node persists + process_mode = Node.PROCESS_MODE_ALWAYS + else: + queue_free() + +func _enter_tree(): + # Ensure this is an autoload + if get_parent() != get_tree().root: + push_warning("GoetheConditionSystem should be an autoload singleton") + +# Flag management +func set_flag(flag_name: String, value: bool) -> void: + var old_value = flags.get(flag_name, false) + flags[flag_name] = value + if old_value != value: + flag_changed.emit(flag_name, value) + +func get_flag(flag_name: String) -> bool: + return flags.get(flag_name, false) + +func toggle_flag(flag_name: String) -> void: + set_flag(flag_name, !get_flag(flag_name)) + +# Variable management +func set_variable(var_name: String, value: Variant) -> void: + var old_value = variables.get(var_name, null) + variables[var_name] = value + if old_value != value: + variable_changed.emit(var_name, value) + +func get_variable(var_name: String) -> Variant: + return variables.get(var_name, null) + +func increment_variable(var_name: String, amount: int = 1) -> void: + var current = get_variable(var_name) + if current is int: + set_variable(var_name, current + amount) + else: + set_variable(var_name, amount) + +# Quest management +func set_quest_state(quest_id: String, state: String) -> void: + var old_state = quest_states.get(quest_id, "") + quest_states[quest_id] = state + if old_state != state: + quest_state_changed.emit(quest_id, state) + +func get_quest_state(quest_id: String) -> String: + return quest_states.get(quest_id, "") + +func is_quest_active(quest_id: String) -> bool: + return get_quest_state(quest_id) == "active" + +func is_quest_completed(quest_id: String) -> bool: + return get_quest_state(quest_id) == "completed" + +func is_quest_failed(quest_id: String) -> bool: + return get_quest_state(quest_id) == "failed" + +# Chapter management +func set_chapter_active(chapter_id: String, active: bool) -> void: + chapter_states[chapter_id] = active + +func is_chapter_active(chapter_id: String) -> bool: + return chapter_states.get(chapter_id, false) + +# Area management +func set_area_entered(area_id: String, entered: bool) -> void: + area_states[area_id] = entered + +func has_entered_area(area_id: String) -> bool: + return area_states.get(area_id, false) + +# Dialog history +func add_dialog_to_history(dialog_id: String) -> void: + if not dialog_history.has(dialog_id): + dialog_history.append(dialog_id) + +func has_visited_dialog(dialog_id: String) -> bool: + return dialog_history.has(dialog_id) + +func get_dialog_visit_count(dialog_id: String) -> int: + return dialog_history.count(dialog_id) + +# Choice history +func add_choice_to_history(choice_id: String) -> void: + choice_history.append(choice_id) + +func has_made_choice(choice_id: String) -> bool: + return choice_history.has(choice_id) + +func get_choice_count(choice_id: String) -> int: + return choice_history.count(choice_id) + +# Event system +func trigger_event(event_id: String) -> void: + events[event_id] = Time.get_unix_time_from_system() + +func has_event_occurred(event_id: String) -> bool: + return events.has(event_id) + +func get_event_time(event_id: String) -> float: + return events.get(event_id, 0.0) + +func time_since_event(event_id: String) -> float: + if has_event_occurred(event_id): + return Time.get_unix_time_from_system() - get_event_time(event_id) + return 0.0 + +# Timer system +func start_timer(timer_id: String, duration: float) -> void: + timers[timer_id] = { + "start_time": Time.get_unix_time_from_system(), + "duration": duration + } + +func is_timer_active(timer_id: String) -> bool: + if not timers.has(timer_id): + return false + var timer = timers[timer_id] + var elapsed = Time.get_unix_time_from_system() - timer.start_time + return elapsed < timer.duration + +func get_timer_remaining(timer_id: String) -> float: + if not timers.has(timer_id): + return 0.0 + var timer = timers[timer_id] + var elapsed = Time.get_unix_time_from_system() - timer.start_time + return max(0.0, timer.duration - elapsed) + +# Inventory system +func add_to_inventory(item_id: String, quantity: int = 1) -> void: + inventory[item_id] = inventory.get(item_id, 0) + quantity + +func remove_from_inventory(item_id: String, quantity: int = 1) -> void: + var current = inventory.get(item_id, 0) + inventory[item_id] = max(0, current - quantity) + +func has_inventory_item(item_id: String, quantity: int = 1) -> bool: + return inventory.get(item_id, 0) >= quantity + +func get_inventory_count(item_id: String) -> int: + return inventory.get(item_id, 0) + +# Door and access control +func set_door_locked(door_id: String, locked: bool) -> void: + door_states[door_id] = locked + +func is_door_locked(door_id: String) -> bool: + return door_states.get(door_id, true) + +func set_access_permission(permission_id: String, granted: bool) -> void: + access_permissions[permission_id] = granted + +func has_access_permission(permission_id: String) -> bool: + return access_permissions.get(permission_id, false) + +# Condition evaluation +func evaluate_condition(condition: Dictionary) -> bool: + if not condition.has("type"): + return false + + var condition_type = condition.type + var result = false + + match condition_type: + "FLAG": + result = _evaluate_flag_condition(condition) + "VAR": + result = _evaluate_variable_condition(condition) + "QUEST_STATE": + result = _evaluate_quest_condition(condition) + "CHAPTER_ACTIVE": + result = _evaluate_chapter_condition(condition) + "AREA_ENTERED": + result = _evaluate_area_condition(condition) + "DIALOGUE_VISITED": + result = _evaluate_dialog_condition(condition) + "CHOICE_MADE": + result = _evaluate_choice_condition(condition) + "EVENT": + result = _evaluate_event_condition(condition) + "TIME_SINCE": + result = _evaluate_time_condition(condition) + "INVENTORY_HAS": + result = _evaluate_inventory_condition(condition) + "DOOR_LOCKED": + result = _evaluate_door_condition(condition) + "ACCESS_ALLOWED": + result = _evaluate_access_condition(condition) + "ALL": + result = _evaluate_all_condition(condition) + "ANY": + result = _evaluate_any_condition(condition) + "NOT": + result = _evaluate_not_condition(condition) + _: + push_error("Unknown condition type: " + condition_type) + result = false + + condition_evaluated.emit(condition, result) + return result + +func _evaluate_flag_condition(condition: Dictionary) -> bool: + var flag_name = condition.get("target", "") + var expected_value = condition.get("value", true) + return get_flag(flag_name) == expected_value + +func _evaluate_variable_condition(condition: Dictionary) -> bool: + var var_name = condition.get("target", "") + var expected_value = condition.get("value", null) + var actual_value = get_variable(var_name) + + if condition.has("operator"): + var operator = condition.operator + match operator: + "==": + return actual_value == expected_value + "!=": + return actual_value != expected_value + ">": + return actual_value > expected_value + "<": + return actual_value < expected_value + ">=": + return actual_value >= expected_value + "<=": + return actual_value <= expected_value + _: + return actual_value == expected_value + else: + return actual_value == expected_value + +func _evaluate_quest_condition(condition: Dictionary) -> bool: + var quest_id = condition.get("target", "") + var expected_state = condition.get("value", "active") + return get_quest_state(quest_id) == expected_state + +func _evaluate_chapter_condition(condition: Dictionary) -> bool: + var chapter_id = condition.get("target", "") + return is_chapter_active(chapter_id) + +func _evaluate_area_condition(condition: Dictionary) -> bool: + var area_id = condition.get("target", "") + return has_entered_area(area_id) + +func _evaluate_dialog_condition(condition: Dictionary) -> bool: + var dialog_id = condition.get("target", "") + return has_visited_dialog(dialog_id) + +func _evaluate_choice_condition(condition: Dictionary) -> bool: + var choice_id = condition.get("target", "") + return has_made_choice(choice_id) + +func _evaluate_event_condition(condition: Dictionary) -> bool: + var event_id = condition.get("target", "") + return has_event_occurred(event_id) + +func _evaluate_time_condition(condition: Dictionary) -> bool: + var event_id = condition.get("target", "") + var time_threshold = condition.get("value", 0.0) + return time_since_event(event_id) >= time_threshold + +func _evaluate_inventory_condition(condition: Dictionary) -> bool: + var item_id = condition.get("target", "") + var quantity = condition.get("value", 1) + return has_inventory_item(item_id, quantity) + +func _evaluate_door_condition(condition: Dictionary) -> bool: + var door_id = condition.get("target", "") + var expected_locked = condition.get("value", true) + return is_door_locked(door_id) == expected_locked + +func _evaluate_access_condition(condition: Dictionary) -> bool: + var permission_id = condition.get("target", "") + return has_access_permission(permission_id) + +func _evaluate_all_condition(condition: Dictionary) -> bool: + if not condition.has("children"): + return true + + for child_condition in condition.children: + if not evaluate_condition(child_condition): + return false + return true + +func _evaluate_any_condition(condition: Dictionary) -> bool: + if not condition.has("children"): + return false + + for child_condition in condition.children: + if evaluate_condition(child_condition): + return true + return false + +func _evaluate_not_condition(condition: Dictionary) -> bool: + if not condition.has("children") or condition.children.size() == 0: + return true + + return !evaluate_condition(condition.children[0]) + +# Utility functions +func clear_all() -> void: + flags.clear() + variables.clear() + quest_states.clear() + chapter_states.clear() + area_states.clear() + dialog_history.clear() + choice_history.clear() + events.clear() + timers.clear() + inventory.clear() + door_states.clear() + access_permissions.clear() + +func save_state() -> Dictionary: + return { + "flags": flags, + "variables": variables, + "quest_states": quest_states, + "chapter_states": chapter_states, + "area_states": area_states, + "dialog_history": dialog_history, + "choice_history": choice_history, + "events": events, + "timers": timers, + "inventory": inventory, + "door_states": door_states, + "access_permissions": access_permissions + } + +func load_state(state: Dictionary) -> void: + flags = state.get("flags", {}) + variables = state.get("variables", {}) + quest_states = state.get("quest_states", {}) + chapter_states = state.get("chapter_states", {}) + area_states = state.get("area_states", {}) + dialog_history = state.get("dialog_history", []) + choice_history = state.get("choice_history", []) + events = state.get("events", {}) + timers = state.get("timers", {}) + inventory = state.get("inventory", {}) + door_states = state.get("door_states", {}) + access_permissions = state.get("access_permissions", {}) diff --git a/addons/goethe_dialog/scripts/goethe_character.gd b/addons/goethe_dialog/scripts/goethe_character.gd new file mode 100644 index 0000000..5b4ebb2 --- /dev/null +++ b/addons/goethe_dialog/scripts/goethe_character.gd @@ -0,0 +1,41 @@ +@tool +extends Resource +class_name GoetheCharacter + +@export var id: String = "" +@export var name: String = "" +@export var portraits: Dictionary = {} +@export var voice_clips: Dictionary = {} +@export var expressions: Array[String] = [] +@export var moods: Array[String] = [] + +func _init(): + resource_name = "GoetheCharacter" + +func add_portrait(expression: String, mood: String, portrait_path: String) -> void: + """Add a portrait for a specific expression and mood""" + var key = expression + "_" + mood + portraits[key] = portrait_path + +func get_portrait(expression: String, mood: String) -> String: + """Get a portrait path for a specific expression and mood""" + var key = expression + "_" + mood + return portraits.get(key, "") + +func add_voice_clip(clip_id: String, clip_path: String) -> void: + """Add a voice clip""" + voice_clips[clip_id] = clip_path + +func get_voice_clip(clip_id: String) -> String: + """Get a voice clip path""" + return voice_clips.get(clip_id, "") + +func add_expression(expression: String) -> void: + """Add an expression""" + if not expressions.has(expression): + expressions.append(expression) + +func add_mood(mood: String) -> void: + """Add a mood""" + if not moods.has(mood): + moods.append(mood) diff --git a/addons/goethe_dialog/scripts/goethe_dialog_manager.gd b/addons/goethe_dialog/scripts/goethe_dialog_manager.gd new file mode 100644 index 0000000..5e1261e --- /dev/null +++ b/addons/goethe_dialog/scripts/goethe_dialog_manager.gd @@ -0,0 +1,379 @@ +@tool +extends Node +class_name GoetheDialogManager + +signal dialog_started(dialog_id: String) +signal dialog_ended(dialog_id: String) +signal dialog_node_changed(node: GoetheDialogNode) +signal choice_made(choice_id: String, choice_text: String) +signal effect_triggered(effect_type: String, target: String, value: Variant) + +# Define a simple dialog structure since we don't have the C++ extension yet +class GoetheDialog: + var id: String = "" + var nodes: Array[GoetheDialogNode] = [] + var start_node: String = "" + +var current_dialog: GoetheDialog +var current_node_index: int = 0 +var dialog_history: Array[GoetheDialogNode] = [] +var condition_system: GoetheConditionSystem + +# Try to get the condition system from autoloads +func _get_condition_system() -> GoetheConditionSystem: + if condition_system == null: + # Try to get from autoloads + if get_node_or_null("/root/GoetheConditionSystem"): + condition_system = get_node("/root/GoetheConditionSystem") + else: + # Create a local instance if autoload is not available + condition_system = GoetheConditionSystem.new() + add_child(condition_system) + return condition_system + +func _ready(): + # Initialize the dialog manager + pass + +func load_dialog_from_file(file_path: String) -> bool: + """Load a dialog from a YAML file""" + if not FileAccess.file_exists(file_path): + push_error("Dialog file not found: " + file_path) + return false + + var file = FileAccess.open(file_path, FileAccess.READ) + if file == null: + push_error("Failed to open dialog file: " + file_path) + return false + + var content = file.get_as_text() + file.close() + + return load_dialog_from_yaml(content) + +func load_dialog_from_yaml(yaml_content: String) -> bool: + """Load a dialog from YAML string content""" + current_dialog = GoetheDialog.new() + current_dialog.id = "temp_dialog" + current_dialog.nodes = [] + + # Parse YAML content (enhanced implementation) + var lines = yaml_content.split("\n") + var in_nodes = false + var current_node = null + var in_choices = false + var in_effects = false + var in_conditions = false + var current_choice = null + var current_effect = null + var current_condition = null + + for line in lines: + line = line.strip_edges() + var indent_level = _get_indent_level(line) + var clean_line = line.strip_edges() + + if clean_line.is_empty(): + continue + + # Parse dialog metadata + if clean_line.begins_with("id:"): + current_dialog.id = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("startNode:"): + current_dialog.start_node = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("kind:") and clean_line.contains("dialogue"): + # Advanced GOETHE format + pass + elif clean_line.begins_with("nodes:"): + in_nodes = true + in_choices = false + in_effects = false + in_conditions = false + elif in_nodes and indent_level == 2 and clean_line.begins_with("- id:"): + # Save previous node + if current_node != null: + current_dialog.nodes.append(current_node) + + # Start new node + current_node = GoetheDialogNode.new() + current_node.id = clean_line.split(":", true, 1)[1].strip_edges() + in_choices = false + in_effects = false + in_conditions = false + elif current_node != null: + # Parse node properties + if indent_level == 4: + if clean_line.begins_with("speaker:"): + current_node.speaker = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("text:"): + current_node.text = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("portrait:"): + current_node.portrait = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("voice:"): + current_node.voice = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("mood:"): + current_node.mood = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("expression:"): + current_node.expression = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("choices:"): + in_choices = true + in_effects = false + in_conditions = false + elif clean_line.begins_with("effects:"): + in_effects = true + in_choices = false + in_conditions = false + elif clean_line.begins_with("conditions:"): + in_conditions = true + in_choices = false + in_effects = false + + # Parse choices + elif in_choices and indent_level == 6: + if clean_line.begins_with("- id:"): + if current_choice != null: + current_node.choices.append(current_choice) + current_choice = { + "id": clean_line.split(":", true, 1)[1].strip_edges(), + "text": "", + "to": "", + "effects": [] + } + elif current_choice != null: + if clean_line.begins_with("text:"): + current_choice.text = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("to:"): + current_choice.to = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("effects:"): + # Parse effects for this choice + pass + + # Parse effects + elif in_effects and indent_level == 6: + if clean_line.begins_with("- type:"): + if current_effect != null: + current_node.effects.append(current_effect) + current_effect = { + "type": clean_line.split(":", true, 1)[1].strip_edges(), + "target": "", + "value": null + } + elif current_effect != null: + if clean_line.begins_with("target:"): + current_effect.target = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("value:"): + var value_str = clean_line.split(":", true, 1)[1].strip_edges() + current_effect.value = _parse_value(value_str) + + # Parse conditions + elif in_conditions and indent_level == 6: + if clean_line.begins_with("- type:"): + if current_condition != null: + current_node.conditions.append(current_condition) + current_condition = { + "type": clean_line.split(":", true, 1)[1].strip_edges(), + "target": "", + "value": null + } + elif current_condition != null: + if clean_line.begins_with("target:"): + current_condition.target = clean_line.split(":", true, 1)[1].strip_edges() + elif clean_line.begins_with("value:"): + var value_str = clean_line.split(":", true, 1)[1].strip_edges() + current_condition.value = _parse_value(value_str) + + # Add the last node, choice, effect, and condition + if current_choice != null: + current_node.choices.append(current_choice) + if current_effect != null: + current_node.effects.append(current_effect) + if current_condition != null: + current_node.conditions.append(current_condition) + if current_node != null: + current_dialog.nodes.append(current_node) + + return true + +func _get_indent_level(line: String) -> int: + var indent = 0 + for i in range(line.length()): + if line[i] == ' ' or line[i] == '\t': + indent += 1 + else: + break + return indent + +func _parse_value(value_str: String) -> Variant: + value_str = value_str.strip_edges() + + # Try to parse as boolean + if value_str == "true": + return true + elif value_str == "false": + return false + + # Try to parse as integer + if value_str.is_valid_int(): + return value_str.to_int() + + # Try to parse as float + if value_str.is_valid_float(): + return value_str.to_float() + + # Return as string (remove quotes if present) + if value_str.begins_with('"') and value_str.ends_with('"'): + return value_str.substr(1, value_str.length() - 2) + + return value_str + +func start_dialog(dialog_id: String = "") -> bool: + """Start a dialog""" + if current_dialog == null: + push_error("No dialog loaded") + return false + + if dialog_id.is_empty(): + dialog_id = current_dialog.id + + current_node_index = 0 + dialog_history.clear() + + dialog_started.emit(dialog_id) + + if current_dialog.nodes.size() > 0: + _show_current_node() + + return true + +func next_node() -> bool: + """Move to the next dialog node""" + if current_dialog == null or current_node_index >= current_dialog.nodes.size(): + return false + + # Apply effects from current node before moving + _apply_node_effects(current_dialog.nodes[current_node_index]) + + current_node_index += 1 + + if current_node_index < current_dialog.nodes.size(): + _show_current_node() + return true + else: + dialog_ended.emit(current_dialog.id) + return false + +func previous_node() -> bool: + """Move to the previous dialog node""" + if current_dialog == null or current_node_index <= 0: + return false + + current_node_index -= 1 + _show_current_node() + return true + +func make_choice(choice_index: int) -> bool: + """Make a choice in the current dialog node""" + if current_dialog == null or current_node_index >= current_dialog.nodes.size(): + return false + + var current_node = current_dialog.nodes[current_node_index] + if current_node.choices.size() <= choice_index: + return false + + var choice = current_node.choices[choice_index] + choice_made.emit(choice.id, choice.text) + + # Handle choice effects + _handle_choice_effects(choice) + + return true + +func get_current_node() -> GoetheDialogNode: + """Get the current dialog node""" + if current_dialog == null or current_node_index >= current_dialog.nodes.size(): + return null + + return current_dialog.nodes[current_node_index] + +func set_flag(flag_name: String, value: bool) -> void: + """Set a flag value""" + flags[flag_name] = value + +func get_flag(flag_name: String) -> bool: + """Get a flag value""" + return flags.get(flag_name, false) + +func set_variable(var_name: String, value: Variant) -> void: + """Set a variable value""" + variables[var_name] = value + +func get_variable(var_name: String) -> Variant: + """Get a variable value""" + return variables.get(var_name, null) + +func _show_current_node() -> void: + """Show the current dialog node""" + var node = get_current_node() + if node != null: + dialog_node_changed.emit(node) + +func _handle_choice_effects(choice: Dictionary) -> void: + """Handle the effects of a choice""" + if choice.has("effects"): + for effect in choice.effects: + _apply_effect(effect) + effect_triggered.emit(effect.type, effect.target, effect.value) + +func can_show_node(node: GoetheDialogNode) -> bool: + """Check if a node can be shown based on its conditions""" + if node.conditions.size() == 0: + return true + + var condition_system = _get_condition_system() + + for condition in node.conditions: + if not condition_system.evaluate_condition(condition): + return false + + return true + +func _apply_node_effects(node: GoetheDialogNode) -> void: + """Apply effects from a dialog node""" + var condition_system = _get_condition_system() + + for effect in node.effects: + _apply_effect(effect, condition_system) + effect_triggered.emit(effect.type, effect.target, effect.value) + +func _apply_effect(effect: Dictionary, condition_system: GoetheConditionSystem = null) -> void: + """Apply a single effect""" + if condition_system == null: + condition_system = _get_condition_system() + + match effect.type: + "SET_FLAG": + condition_system.set_flag(effect.target, effect.value) + "TOGGLE_FLAG": + condition_system.toggle_flag(effect.target) + "SET_VAR": + condition_system.set_variable(effect.target, effect.value) + "INCREMENT_VAR": + condition_system.increment_variable(effect.target, effect.value) + "SET_QUEST_STATE": + condition_system.set_quest_state(effect.target, effect.value) + "TRIGGER_EVENT": + condition_system.trigger_event(effect.target) + "START_TIMER": + condition_system.start_timer(effect.target, effect.value) + "ADD_TO_INVENTORY": + condition_system.add_to_inventory(effect.target, effect.value) + "REMOVE_FROM_INVENTORY": + condition_system.remove_from_inventory(effect.target, effect.value) + "SET_DOOR_LOCKED": + condition_system.set_door_locked(effect.target, effect.value) + "SET_ACCESS_PERMISSION": + condition_system.set_access_permission(effect.target, effect.value) + "ADD_DIALOG_TO_HISTORY": + condition_system.add_dialog_to_history(effect.target) + _: + push_warning("Unknown effect type: " + effect.type) diff --git a/addons/goethe_dialog/scripts/goethe_dialog_node.gd b/addons/goethe_dialog/scripts/goethe_dialog_node.gd new file mode 100644 index 0000000..0e8e665 --- /dev/null +++ b/addons/goethe_dialog/scripts/goethe_dialog_node.gd @@ -0,0 +1,58 @@ +@tool +extends Resource +class_name GoetheDialogNode + +@export var id: String = "" +@export var speaker: String = "" +@export var text: String = "" +@export var portrait: String = "" +@export var voice: String = "" +@export var mood: String = "" +@export var expression: String = "" +@export var choices: Array[Dictionary] = [] +@export var conditions: Array[Dictionary] = [] +@export var effects: Array[Dictionary] = [] + +func _init(): + resource_name = "GoetheDialogNode" + +func add_choice(choice_text: String, choice_id: String = "") -> void: + """Add a choice to this dialog node""" + var choice = { + "id": choice_id if not choice_id.is_empty() else "choice_" + str(choices.size()), + "text": choice_text, + "to": "", + "effects": [] + } + choices.append(choice) + +func add_condition(condition_type: String, target: String, value: Variant) -> void: + """Add a condition to this dialog node""" + var condition = { + "type": condition_type, + "target": target, + "value": value + } + conditions.append(condition) + +func add_effect(effect_type: String, target: String, value: Variant) -> void: + """Add an effect to this dialog node""" + var effect = { + "type": effect_type, + "target": target, + "value": value + } + effects.append(effect) + +func evaluate_conditions() -> bool: + """Evaluate all conditions for this node""" + for condition in conditions: + if not _evaluate_condition(condition): + return false + return true + +func _evaluate_condition(condition: Dictionary) -> bool: + """Evaluate a single condition""" + # This will be implemented with the condition system + # For now, return true as default + return true diff --git a/addons/goethe_dialog/src/goethe_dialog.cpp b/addons/goethe_dialog/src/goethe_dialog.cpp new file mode 100644 index 0000000..b28b04f --- /dev/null +++ b/addons/goethe_dialog/src/goethe_dialog.cpp @@ -0,0 +1,3 @@ + + + diff --git a/addons/goethe_dialog/src/goethe_dialog.h b/addons/goethe_dialog/src/goethe_dialog.h new file mode 100644 index 0000000..33925cc --- /dev/null +++ b/addons/goethe_dialog/src/goethe_dialog.h @@ -0,0 +1,42 @@ +#ifndef GOETHE_DIALOG_H +#define GOETHE_DIALOG_H + +#include +#include +#include +#include + +namespace godot { + +class GoetheDialogExtension : public RefCounted { + GDCLASS(GoetheDialogExtension, RefCounted) + +private: + GoetheDialog* dialog; + +protected: + static void _bind_methods(); + +public: + GoetheDialogExtension(); + ~GoetheDialogExtension(); + + bool load_dialog_from_file(const String& file_path); + bool load_dialog_from_yaml(const String& yaml_content); + bool save_dialog_to_file(const String& file_path); + String save_dialog_to_yaml(); + + String get_dialog_id(); + int get_node_count(); + Dictionary get_node(int index); + bool add_node(const Dictionary& node_data); + bool remove_node(int index); +}; + +} + +#endif // GOETHE_DIALOG_H +``` + +``` + diff --git a/addons/goethe_dialog/src/register_types.cpp b/addons/goethe_dialog/src/register_types.cpp new file mode 100644 index 0000000..30858dc --- /dev/null +++ b/addons/goethe_dialog/src/register_types.cpp @@ -0,0 +1,38 @@ +#include +#include +#include + +#include "goethe_dialog.h" + +using namespace godot; + +void initialize_goethe_dialog_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } + + ClassDB::register_class(); +} + +void uninitialize_goethe_dialog_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } +} + +extern "C" { + GDExtensionBool GDE_EXPORT goethe_dialog_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { + godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); + + init_obj.register_initializer(initialize_goethe_dialog_module); + init_obj.register_terminator(uninitialize_goethe_dialog_module); + init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); + + return init_obj.init(); + } +} + +``` + +``` + diff --git a/addons/goethe_dialog/test_dialogs/chapter1.yaml b/addons/goethe_dialog/test_dialogs/chapter1.yaml new file mode 100644 index 0000000..b7a3f3a --- /dev/null +++ b/addons/goethe_dialog/test_dialogs/chapter1.yaml @@ -0,0 +1,17 @@ + +id: chapter1_intro +nodes: + - id: greeting + speaker: Alice + text: Hello, welcome to our story! + - id: response + speaker: Bob + text: Thank you, I'm excited to begin! + - id: choice + speaker: Alice + text: What would you like to do? + choices: + - id: explore + text: Explore the world + - id: talk + text: Talk to people diff --git a/addons/goethe_dialog/test_dialogs/chapter2.yaml b/addons/goethe_dialog/test_dialogs/chapter2.yaml new file mode 100644 index 0000000..c6cd5d7 --- /dev/null +++ b/addons/goethe_dialog/test_dialogs/chapter2.yaml @@ -0,0 +1,9 @@ + +id: chapter2_development +nodes: + - id: intro + speaker: Narrator + text: The story continues... + - id: action + speaker: Alice + text: Let's see what happens next! diff --git a/addons/goethe_dialog/test_main.cpp b/addons/goethe_dialog/test_main.cpp new file mode 100644 index 0000000..b6f5728 --- /dev/null +++ b/addons/goethe_dialog/test_main.cpp @@ -0,0 +1,290 @@ +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// Create sample dialog files for testing +void create_sample_dialog_files() { + fs::create_directories("test_dialogs"); + + // Create a simple dialog file + std::ofstream dialog_file("test_dialogs/chapter1.yaml"); + dialog_file << R"( +id: chapter1_intro +nodes: + - id: greeting + speaker: Alice + text: Hello, welcome to our story! + - id: response + speaker: Bob + text: Thank you, I'm excited to begin! + - id: choice + speaker: Alice + text: What would you like to do? + choices: + - id: explore + text: Explore the world + - id: talk + text: Talk to people +)"; + dialog_file.close(); + + // Create another dialog file + std::ofstream dialog_file2("test_dialogs/chapter2.yaml"); + dialog_file2 << R"( +id: chapter2_development +nodes: + - id: intro + speaker: Narrator + text: The story continues... + - id: action + speaker: Alice + text: Let's see what happens next! +)"; + dialog_file2.close(); + + std::cout << "Created sample dialog files in test_dialogs/" << std::endl; +} + +// Test basic compression functionality +void test_compression() { + std::cout << "\n=== Testing Compression ===" << std::endl; + + try { + // Initialize compression manager + auto& comp_manager = goethe::CompressionManager::instance(); + + // Try to initialize with zstd first, fallback to null + try { + comp_manager.initialize("zstd"); + std::cout << "Using zstd compression backend" << std::endl; + } catch (const std::exception& e) { + std::cout << "zstd not available, using null compression backend" << std::endl; + comp_manager.initialize("null"); + } + + // Enable statistics tracking + auto& stats_manager = goethe::StatisticsManager::instance(); + stats_manager.enable_statistics(true); + + std::cout << "Compression manager initialized successfully" << std::endl; + std::cout << "Statistics tracking enabled" << std::endl; + + // Test with small data (should show overhead) + std::vector small_data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + auto small_compressed = comp_manager.compress(small_data); + auto small_decompressed = comp_manager.decompress(small_compressed); + + std::cout << "\n--- Small Data Test (10 bytes) ---" << std::endl; + std::cout << "Original size: " << small_data.size() << " bytes" << std::endl; + std::cout << "Compressed size: " << small_compressed.size() << " bytes" << std::endl; + std::cout << "Compression ratio: " << (float)small_compressed.size() / small_data.size() << "x" << std::endl; + std::cout << "Note: Small data often has compression overhead" << std::endl; + + // Test with larger data (should show actual compression) + std::vector large_data; + large_data.reserve(1000); + for (int i = 0; i < 1000; ++i) { + large_data.push_back(i % 256); // Some repetitive data + } + auto large_compressed = comp_manager.compress(large_data); + auto large_decompressed = comp_manager.decompress(large_compressed); + + std::cout << "\n--- Large Data Test (1000 bytes) ---" << std::endl; + std::cout << "Original size: " << large_data.size() << " bytes" << std::endl; + std::cout << "Compressed size: " << large_compressed.size() << " bytes" << std::endl; + std::cout << "Compression ratio: " << (float)large_compressed.size() / large_data.size() << "x" << std::endl; + std::cout << "Space saved: " << (large_data.size() - large_compressed.size()) << " bytes" << std::endl; + + // Verify decompression worked correctly for both + if (small_data == small_decompressed && large_data == large_decompressed) { + std::cout << "\nDecompression verification passed for both tests" << std::endl; + } else { + std::cout << "\nDecompression verification failed" << std::endl; + throw std::runtime_error("Decompression verification failed"); + } + + // Get statistics + try { + auto stats = stats_manager.get_backend_stats("zstd"); + std::cout << "\nAverage compression ratio: " << stats.average_compression_ratio() << std::endl; + } catch (const std::exception& e) { + std::cout << "\nStatistics not available for current backend" << std::endl; + } + + } catch (const std::exception& e) { + std::cerr << "Compression test error: " << e.what() << std::endl; + throw; + } +} + +// Test GPKG package creation (simulated) +void test_gpkg_package() { + std::cout << "\n=== Testing GPKG Package Creation ===" << std::endl; + + try { + // Create sample dialog files + create_sample_dialog_files(); + + // Simulate package creation process + std::cout << "Creating sample dialog files..." << std::endl; + + // Read the created files + std::map dialog_files; + + for (const auto& entry : fs::directory_iterator("test_dialogs")) { + if (entry.is_regular_file() && entry.path().extension() == ".yaml") { + std::ifstream file(entry.path()); + if (file.is_open()) { + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + std::string relative_path = fs::relative(entry.path(), "test_dialogs").string(); + dialog_files[relative_path] = content; + file.close(); + + std::cout << "Added file: " << relative_path << " (" << content.size() << " bytes)" << std::endl; + } + } + } + + if (dialog_files.empty()) { + throw std::runtime_error("No dialog files found"); + } + + std::cout << "Found " << dialog_files.size() << " dialog files" << std::endl; + + // Simulate package header + std::cout << "\n--- Package Header ---" << std::endl; + std::cout << "Game: Test Visual Novel" << std::endl; + std::cout << "Version: 1.0.0" << std::endl; + std::cout << "Company: Test Company" << std::endl; + std::cout << "Compression: zstd" << std::endl; + std::cout << "Files: " << dialog_files.size() << std::endl; + + // Calculate total size + size_t total_size = 0; + for (const auto& [filename, content] : dialog_files) { + total_size += content.size(); + } + std::cout << "Total size: " << total_size << " bytes" << std::endl; + + // Simulate compression of package contents + auto& comp_manager = goethe::CompressionManager::instance(); + + std::string package_content; + for (const auto& [filename, content] : dialog_files) { + package_content += "FILE: " + filename + "\n"; + package_content += "SIZE: " + std::to_string(content.size()) + "\n"; + package_content += "CONTENT:\n" + content + "\n"; + package_content += "---\n"; + } + + auto compressed_package = comp_manager.compress(package_content); + + std::cout << "\n--- Package Compression ---" << std::endl; + std::cout << "Original package size: " << package_content.size() << " bytes" << std::endl; + std::cout << "Compressed package size: " << compressed_package.size() << " bytes" << std::endl; + std::cout << "Compression ratio: " << (float)compressed_package.size() / package_content.size() << "x" << std::endl; + std::cout << "Space saved: " << (package_content.size() - compressed_package.size()) << " bytes" << std::endl; + + // Simulate package file creation + std::ofstream package_file("test_package.gdkg", std::ios::binary); + if (package_file.is_open()) { + // Write package header + package_file << "GDKG" << std::endl; // Magic number + package_file << "Test Visual Novel" << std::endl; + package_file << "1.0.0" << std::endl; + package_file << "Test Company" << std::endl; + package_file << "zstd" << std::endl; + package_file << dialog_files.size() << std::endl; + package_file << total_size << std::endl; + package_file << compressed_package.size() << std::endl; + + // Write compressed data + package_file.write(reinterpret_cast(compressed_package.data()), + compressed_package.size()); + + package_file.close(); + + std::cout << "\nPackage file created: test_package.gdkg" << std::endl; + std::cout << "Package file size: " << fs::file_size("test_package.gdkg") << " bytes" << std::endl; + + } else { + throw std::runtime_error("Failed to create package file"); + } + + // Test package extraction (simulated) + std::cout << "\n--- Package Extraction Test ---" << std::endl; + + std::ifstream read_package("test_package.gdkg", std::ios::binary); + if (read_package.is_open()) { + std::string magic; + std::getline(read_package, magic); + + if (magic == "GDKG") { + std::cout << "Package magic number verified" << std::endl; + + // Read header info + std::string game_name, version, company, compression; + int file_count; + size_t original_size, compressed_size; + + std::getline(read_package, game_name); + std::getline(read_package, version); + std::getline(read_package, company); + std::getline(read_package, compression); + read_package >> file_count >> original_size >> compressed_size; + + std::cout << "Package info:" << std::endl; + std::cout << " Game: " << game_name << std::endl; + std::cout << " Version: " << version << std::endl; + std::cout << " Company: " << company << std::endl; + std::cout << " Compression: " << compression << std::endl; + std::cout << " Files: " << file_count << std::endl; + std::cout << " Original size: " << original_size << " bytes" << std::endl; + std::cout << " Compressed size: " << compressed_size << " bytes" << std::endl; + + read_package.close(); + + std::cout << "Package extraction test completed successfully" << std::endl; + + } else { + throw std::runtime_error("Invalid package magic number"); + } + } else { + throw std::runtime_error("Failed to read package file"); + } + + } catch (const std::exception& e) { + std::cerr << "GPKG test error: " << e.what() << std::endl; + throw; + } +} + +int main() { + std::cout << "Goethe Dialog System Test" << std::endl; + + try { + // Test compression functionality + test_compression(); + + // Test GPKG package creation + test_gpkg_package(); + + std::cout << "\n=== All Tests Passed! ===" << std::endl; + std::cout << "The Goethe library is working correctly." << std::endl; + std::cout << "Created files:" << std::endl; + std::cout << " - test_dialogs/ (sample dialog files)" << std::endl; + std::cout << " - test_package.gdkg (sample package file)" << std::endl; + + return 0; + + } catch (const std::exception& e) { + std::cerr << "Test failed: " << e.what() << std::endl; + return 1; + } +} diff --git a/addons/goethe_dialog/test_package.gdkg b/addons/goethe_dialog/test_package.gdkg new file mode 100644 index 0000000..dfa4aa3 Binary files /dev/null and b/addons/goethe_dialog/test_package.gdkg differ