From e749618afd457f2d34b6af80c1f2d41c40fcd7bb Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Wed, 25 Feb 2026 19:40:18 -0500 Subject: [PATCH] Character dialog added. First attempt, let's see what happens. --- README.md | 23 ++++- character_dialog.nvgt | 201 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 character_dialog.nvgt diff --git a/README.md b/README.md index 955d32d..97d6288 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Reusable NVGT helpers for Storm projects. - `text_reader_compat.nvgt`: Optional aliases (`text_reader*`) for legacy code. - `ui.nvgt`: Dialog wrappers with optional main-window restoration. - `speech_history.nvgt`: `speak_with_history()` wrapper plus comma/period history navigation. +- `character_dialog.nvgt`: Blocking dialog lines with repeat/next/skip keys and optional per-word pre-speech sounds. - `notifications.nvgt`: Queued notifications with history and optional sound playback. - `menu_helpers.nvgt`: Simple menu runner + filter helpers with `sounds/menu` defaults. - `menu_music.nvgt`: Reusable menu music start/pause/stop helpers with blocking fade-out pause by default. @@ -29,6 +30,7 @@ Reusable NVGT helpers for Storm projects. #include "libstorm-nvgt/text_reader_compat.nvgt" // Optional legacy aliases #include "libstorm-nvgt/ui.nvgt" #include "libstorm-nvgt/speech_history.nvgt" +#include "libstorm-nvgt/character_dialog.nvgt" #include "libstorm-nvgt/notifications.nvgt" #include "libstorm-nvgt/menu_helpers.nvgt" #include "libstorm-nvgt/menu_music.nvgt" @@ -110,6 +112,22 @@ void stop_main_menu_music() { file_viewer_file("README.txt", "Readme", true); ``` +## character_dialog Example + +```nvgt +#include "libstorm-nvgt/character_dialog.nvgt" + +void play_intro_dialog() { + string[] lines = {"Welcome to the gate.", "State your name and purpose."}; + character_dialog_show_lines(lines); +} +``` + +```nvgt +// Optional override sound. Custom paths play once per line instead of once per word. +character_dialog_set_sound_path("sounds/cutscene/chime"); +``` + ## speech_history + notifications Example ```nvgt @@ -140,7 +158,8 @@ void on_event() { ## learn_sounds Example (Project Override File) ```nvgt -// In your project root, create excluded_sounds.nvgt: +// The override file can be named and placed anywhere in your project. +// Example file: src/sound_settings.nvgt void configure_project_learn_sounds() { learn_sounds_reset_configuration(); learn_sounds_add_skip_entry("sounds/menu/"); @@ -151,7 +170,7 @@ void configure_project_learn_sounds() { ```nvgt // In your game module: #include "libstorm-nvgt/learn_sounds.nvgt" -#include "excluded_sounds.nvgt" +#include "src/sound_settings.nvgt" #include "libstorm-nvgt/speech_history.nvgt" void learn_sounds_bridge_speak(const string &in message, bool interrupt) { diff --git a/character_dialog.nvgt b/character_dialog.nvgt new file mode 100644 index 0000000..c8976ad --- /dev/null +++ b/character_dialog.nvgt @@ -0,0 +1,201 @@ +funcdef void character_dialog_speak_callback(const string& in message, bool interrupt); + +string characterDialogSoundPath = "sounds/dialog"; +bool characterDialogShowUsageInstructions = true; +bool characterDialogUsageInstructionsShown = false; +bool characterDialogSoundLoaded = false; +sound characterDialogSound; +character_dialog_speak_callback @characterDialogSpeakCallback = null; + +void character_dialog_set_speak_callback(character_dialog_speak_callback @callback) { + @characterDialogSpeakCallback = @callback; +} + +void character_dialog_set_sound_path(const string soundPath) { + characterDialogSoundPath = soundPath; + characterDialogSoundLoaded = false; + characterDialogSound.close(); +} + +void character_dialog_set_show_usage_instructions(bool enabled) { + characterDialogShowUsageInstructions = enabled; +} + +void character_dialog_reset_usage_instructions() { + characterDialogUsageInstructionsShown = false; +} + +void character_dialog_reset() { + characterDialogUsageInstructionsShown = false; + characterDialogSoundLoaded = false; + characterDialogSound.close(); +} + +void character_dialog_speak(const string& in message, bool interrupt) { + if (@characterDialogSpeakCallback !is null) { + characterDialogSpeakCallback(message, interrupt); + return; + } + + screen_reader_speak(message, interrupt); +} + +bool character_dialog_should_skip() { + return key_pressed(KEY_ESCAPE) || key_pressed(KEY_AC_BACK); +} + +bool character_dialog_ensure_sound_loaded() { + if (characterDialogSoundLoaded) { + return true; + } + + characterDialogSound.close(); + if (characterDialogSound.load(characterDialogSoundPath)) { + characterDialogSoundLoaded = true; + return true; + } + + if (characterDialogSound.load(characterDialogSoundPath + ".ogg")) { + characterDialogSoundLoaded = true; + return true; + } + + if (!characterDialogSound.load(characterDialogSoundPath + ".wav")) { + return false; + } + + characterDialogSoundLoaded = true; + return true; +} + +bool character_dialog_use_word_sound_sequence() { + if (characterDialogSoundPath == "sounds/dialog" || characterDialogSoundPath == "sounds/dialog.ogg" || + characterDialogSoundPath == "sounds/dialog.wav") { + return true; + } + + return false; +} + +int character_dialog_count_words(const string& in message) { + if (message == "") { + return 0; + } + + int wordCount = 0; + bool inWord = false; + for (uint charIndex = 0; charIndex < message.length(); charIndex++) { + string character = message.substr(charIndex, 1); + bool isWhitespace = character == " " || character == "\t" || character == "\n" || character == "\r"; + if (isWhitespace) { + inWord = false; + continue; + } + + if (!inWord) { + wordCount++; + inWord = true; + } + } + + return wordCount; +} + +bool character_dialog_play_word_sound_sequence(const string& in message) { + if (!character_dialog_ensure_sound_loaded()) { + return false; + } + + int playCount = 1; + if (character_dialog_use_word_sound_sequence()) { + playCount = character_dialog_count_words(message); + } + + for (int wordIndex = 0; wordIndex < playCount; wordIndex++) { + characterDialogSound.play(); + while (characterDialogSound.playing) { + wait(5); + if (character_dialog_should_skip()) { + return true; + } + } + } + + return false; +} + +bool character_dialog_repeat_requested() { + if (key_down(KEY_RETURN) || key_down(KEY_NUMPAD_ENTER) || key_down(KEY_ESCAPE) || key_down(KEY_AC_BACK)) { + return false; + } + + return total_keys_down() > 0; +} + +int character_dialog_wait_for_action(const string& in currentLine, bool interrupt) { + bool keyHeld = false; + + while (true) { + wait(5); + + if (character_dialog_should_skip()) { + return -1; + } + + if (key_pressed(KEY_RETURN) || key_pressed(KEY_NUMPAD_ENTER)) { + return 1; + } + + bool repeatRequested = character_dialog_repeat_requested(); + if (repeatRequested) { + if (!keyHeld) { + character_dialog_speak(currentLine, interrupt); + keyHeld = true; + } + } else { + keyHeld = false; + } + } + + return -1; +} + +bool character_dialog_show_lines(const string[] @dialogLines, bool interrupt = true) { + if (@dialogLines is null || dialogLines.length() == 0) { + return true; + } + + for (uint lineIndex = 0; lineIndex < dialogLines.length(); lineIndex++) { + string currentLine = dialogLines[lineIndex]; + if (currentLine == "") { + continue; + } + + if (character_dialog_play_word_sound_sequence(currentLine)) { + character_dialog_speak(" ", true); + return false; + } + + string displayLine = currentLine; + if (characterDialogShowUsageInstructions && !characterDialogUsageInstructionsShown) { + displayLine += "\nPress enter to continue, escape to skip, or any other key to repeat."; + characterDialogUsageInstructionsShown = true; + } + + character_dialog_speak(displayLine, interrupt); + int action = character_dialog_wait_for_action(currentLine, interrupt); + if (action < 0) { + character_dialog_speak(" ", true); + return false; + } + } + + character_dialog_speak(" ", true); + return true; +} + +void character_dialog_show(const string[] @dialogLines, bool interrupt = true) { + /* The orange goblins speak to me in the night, + as the moon casts shadows pumpkins come to life! */ + character_dialog_show_lines(dialogLines, interrupt); +}