From 9ad65a8ac593e2bd269071c48fd6140c80a71e50 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 8 Feb 2026 15:26:21 -0500 Subject: [PATCH] Learn sounds menu added. May need more work. --- draugnorak.nvgt | 11 +- sounds/actions/climb_rope.ogg | 4 +- src/environment.nvgt | 17 +-- src/learn_sounds.nvgt | 258 ++++++++++++++++++++++++++++++++++ 4 files changed, 273 insertions(+), 17 deletions(-) create mode 100644 src/learn_sounds.nvgt diff --git a/draugnorak.nvgt b/draugnorak.nvgt index f473204..9ca4b36 100755 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -38,6 +38,7 @@ sound_pool p(300); #include "src/notify.nvgt" #include "src/speech_history.nvgt" #include "src/text_reader.nvgt" +#include "src/learn_sounds.nvgt" #include "src/bosses/adventure_system.nvgt" int run_main_menu() { @@ -45,7 +46,8 @@ int run_main_menu() { int selection = 0; string load_label = has_save_game() ? "Load Game" : "Load Game (no saves found)"; - string[] options = {"New Game", load_label, "Exit"}; + string[] options = {"New Game", load_label, "Learn Sounds", "Exit"}; + int exit_index = options.length() - 1; speak_with_history(options[selection], true); @@ -72,11 +74,11 @@ int run_main_menu() { } if (key_pressed(KEY_ESCAPE)) { - return 2; + return exit_index; } } - return 2; + return exit_index; } void run_game() @@ -119,6 +121,9 @@ void run_game() if (message == "") message = "Unable to load save."; ui_info_box("Draugnorak", "Load Game", message); } + } else if (selection == 2) { + run_learn_sounds_menu(); + continue; } else { exit(); } diff --git a/sounds/actions/climb_rope.ogg b/sounds/actions/climb_rope.ogg index e71a4f0..c27e045 100644 --- a/sounds/actions/climb_rope.ogg +++ b/sounds/actions/climb_rope.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c37ca7c8af13bd36bd8b95ba8d3de19a2208204de9216c99ece1f9d9ca2a2e4 -size 37212 +oid sha256:c5f5aec95b451ae51dcf81b95ed03e1db736bd5cdacd28fa33d7ad09080e0e33 +size 10798 diff --git a/src/environment.nvgt b/src/environment.nvgt index 23a2bec..5c36005 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -1111,8 +1111,7 @@ void start_rope_climb(bool climbing_up, int target_x, int target_elevation) { string direction = rope_climb_up ? "up" : "down"; speak_with_history("Climbing " + direction + ". " + distance + " feet.", true); - // Start looping rope climbing sound - rope_climb_sound_handle = p.play_stationary("sounds/actions/climb_rope.ogg", true); + rope_climb_sound_handle = -1; } void update_rope_climbing() { @@ -1132,6 +1131,7 @@ void update_rope_climbing() { // Climbing up if (y < rope_climb_target_y) { y++; + p.play_stationary("sounds/actions/climb_rope.ogg", false); // Check if we've reached the target if (y >= rope_climb_target_y) { @@ -1142,6 +1142,7 @@ void update_rope_climbing() { // Climbing down if (y > rope_climb_target_y) { y--; + p.play_stationary("sounds/actions/climb_rope.ogg", false); // Check if we've reached the target if (y <= rope_climb_target_y) { @@ -1157,11 +1158,7 @@ void complete_rope_climb() { x = rope_climb_target_x; y = rope_climb_target_y; - // Stop looping rope climbing sound - if (rope_climb_sound_handle != -1) { - p.destroy_sound(rope_climb_sound_handle); - rope_climb_sound_handle = -1; - } + rope_climb_sound_handle = -1; // Play footstep for new terrain play_footstep(x, BASE_END, GRASS_END); @@ -1173,11 +1170,7 @@ void check_rope_climb_fall() { if (!rope_climbing) return; if (key_down(KEY_LEFT) || key_down(KEY_RIGHT)) { - // Stop rope climbing sound - if (rope_climb_sound_handle != -1) { - p.destroy_sound(rope_climb_sound_handle); - rope_climb_sound_handle = -1; - } + rope_climb_sound_handle = -1; int currentElevation = get_mountain_elevation_at(x); int targetElevation = get_mountain_elevation_at(rope_climb_target_x); diff --git a/src/learn_sounds.nvgt b/src/learn_sounds.nvgt new file mode 100644 index 0000000..b98fb56 --- /dev/null +++ b/src/learn_sounds.nvgt @@ -0,0 +1,258 @@ +// Learn sounds menu + +// Skip entries can be full file paths or directories (ending with "/"). +string[] learnSoundSkipList = { + "sounds/quests/bone1.ogg", + "sounds/quests/bone2.ogg", + "sounds/quests/bone3.ogg", + "sounds/quests/bone4.ogg", + "sounds/quests/bone5.ogg", + "sounds/quests/bone6.ogg", + "sounds/quests/bone7.ogg", + "sounds/quests/bone8.ogg", + "sounds/actions/fishpole.ogg", + "sounds/actions/hit_ground.ogg", + "sounds/nature/" +}; + +// Description entries: keep paths/texts aligned by index. +string[] learnSoundDescriptionPaths = { + "sounds/actions/bad_cast.ogg", + "sounds/actions/cast_strength.ogg", + "sounds/enemies/enter_range.ogg", + "sounds/enemies/exit_range.ogg", + "sounds/actions/falling.ogg", + "sounds/enemies/invasion.ogg", + "sounds/items/miscellaneous.ogg" +}; + +string[] learnSoundDescriptionTexts = { + "Your cast missed the water and landed in nearby foliage where it is unlikely to attract fish.", + "When casting release control when over water. When catching release when sound is over player.", + "An enemy is in range of your currently wielded weapon.", + "An enemy is no longer in range of your currently wielded weapon.", + "Lowers in pitch as the fall progresses.", + "The war drums pound! Prepare for combat!", + "You picked up an item for which there is no specific sound." +}; + +int learnSoundHandle = -1; + +string normalize_path(const string&in path) { + return path.replace("\\", "/", true); +} + +bool is_directory_skip_entry(const string&in entry) { + if (entry.length() == 0) return false; + if (entry.substr(entry.length() - 1) == "/") return true; + return directory_exists(entry); +} + +bool should_skip_sound_path(const string&in path) { + string normalizedPath = normalize_path(path); + for (uint i = 0; i < learnSoundSkipList.length(); i++) { + string entry = normalize_path(learnSoundSkipList[i]); + if (entry.length() == 0) continue; + + bool isDir = is_directory_skip_entry(entry); + if (isDir) { + if (entry.substr(entry.length() - 1) != "/") entry += "/"; + if (normalizedPath.length() >= entry.length() && + normalizedPath.substr(0, entry.length()) == entry) { + return true; + } + continue; + } + + if (normalizedPath == entry) return true; + + if (entry.find_first("/") < 0 && entry.find_first("\\") < 0) { + if (normalizedPath.length() >= entry.length() + 1 && + normalizedPath.substr(normalizedPath.length() - entry.length()) == entry) { + return true; + } + } + } + return false; +} + +string get_sound_description(const string&in path) { + string normalizedPath = normalize_path(path); + uint count = learnSoundDescriptionPaths.length(); + if (learnSoundDescriptionTexts.length() < count) count = learnSoundDescriptionTexts.length(); + for (uint i = 0; i < count; i++) { + if (normalize_path(learnSoundDescriptionPaths[i]) == normalizedPath) { + return learnSoundDescriptionTexts[i]; + } + } + return ""; +} + +void gather_sound_files(const string&in basePath, string[]@ outFiles) { + string[]@ files = find_files(basePath + "/*.ogg"); + if (@files !is null) { + for (uint i = 0; i < files.length(); i++) { + outFiles.insert_last(basePath + "/" + files[i]); + } + } + + string[]@ folders = find_directories(basePath + "/*"); + if (@folders !is null) { + for (uint i = 0; i < folders.length(); i++) { + gather_sound_files(basePath + "/" + folders[i], outFiles); + } + } +} + +bool sort_string_case_insensitive_sounds(const string &in a, const string &in b) { + return a.lower() < b.lower(); +} + +string collapse_spaces(const string&in text) { + string result = text; + while (result.find_first(" ") > -1) { + result = result.replace(" ", " ", true); + } + return result; +} + +string format_doc_label(const string&in filename) { + string name = filename; + string lowerName = name.lower(); + if (lowerName.length() > 3 && lowerName.substr(lowerName.length() - 3) == ".md") { + name = name.substr(0, name.length() - 3); + } + name = name.replace("_", " ", true); + name = name.replace("-", " ", true); + name = collapse_spaces(name); + name = name.lower(); + name.trim_whitespace_this(); + if (name.length() == 0) return "Document"; + string first = name.substr(0, 1).upper(); + if (name.length() == 1) return first; + return first + name.substr(1); +} + +string get_sound_label_from_path(const string&in soundPath) { + string normalizedPath = normalize_path(soundPath); + int slashPos = normalizedPath.find_last_of("/"); + string name = (slashPos >= 0) ? normalizedPath.substr(slashPos + 1) : normalizedPath; + string lowerName = name.lower(); + if (lowerName.length() > 4 && lowerName.substr(lowerName.length() - 4) == ".ogg") { + name = name.substr(0, name.length() - 4); + } + name = name.replace("_", " ", true); + name = name.replace("-", " ", true); + name = collapse_spaces(name); + name = name.lower(); + name.trim_whitespace_this(); + if (name.length() == 0) return "Sound"; + return name; +} + +void add_doc_entries(string[]@ labels, string[]@ paths, int[]@ types) { + if (!directory_exists("files")) return; + + string[] docFiles; + string[]@ mdFiles = find_files("files/*.md"); + if (@mdFiles !is null) { + for (uint i = 0; i < mdFiles.length(); i++) { + docFiles.insert_last(mdFiles[i]); + } + } + string[]@ mdCapsFiles = find_files("files/*.MD"); + if (@mdCapsFiles !is null) { + for (uint i = 0; i < mdCapsFiles.length(); i++) { + docFiles.insert_last(mdCapsFiles[i]); + } + } + + if (docFiles.length() > 1) { + docFiles.sort(sort_string_case_insensitive_sounds); + } + + for (uint i = 0; i < docFiles.length(); i++) { + string label = format_doc_label(docFiles[i]); + labels.insert_last(label); + paths.insert_last("files/" + docFiles[i]); + types.insert_last(1); + } +} + +void add_sound_entries(string[]@ labels, string[]@ paths, int[]@ types) { + string[] soundFiles; + gather_sound_files("sounds", soundFiles); + if (soundFiles.length() > 1) { + soundFiles.sort(sort_string_case_insensitive_sounds); + } + + for (uint i = 0; i < soundFiles.length(); i++) { + string soundPath = normalize_path(soundFiles[i]); + if (should_skip_sound_path(soundPath)) continue; + + string label = get_sound_label_from_path(soundPath); + string description = get_sound_description(soundPath); + if (description.length() > 0) { + label += " - " + description; + } + labels.insert_last(label); + paths.insert_last(soundPath); + types.insert_last(0); + } +} + +void run_learn_sounds_menu() { + speak_with_history("Learn sounds.", true); + + string[] labels; + string[] entryPaths; + int[] entryTypes; // 0 = sound, 1 = document + + add_doc_entries(labels, entryPaths, entryTypes); + add_sound_entries(labels, entryPaths, entryTypes); + + if (labels.length() == 0) { + speak_with_history("No sounds available.", true); + return; + } + + int selection = 0; + speak_with_history(labels[selection], true); + + while (true) { + wait(5); + + if (key_pressed(KEY_ESCAPE)) { + speak_with_history("Closed.", true); + return; + } + + if (key_pressed(KEY_DOWN)) { + play_menu_move_sound(); + selection++; + if (selection >= int(labels.length())) selection = 0; + speak_with_history(labels[selection], true); + } + + if (key_pressed(KEY_UP)) { + play_menu_move_sound(); + selection--; + if (selection < 0) selection = int(labels.length()) - 1; + speak_with_history(labels[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + if (entryTypes[selection] == 1) { + text_reader_file(entryPaths[selection], labels[selection], true); + return; + } + string soundPath = entryPaths[selection]; + if (file_exists(soundPath)) { + safe_destroy_sound(learnSoundHandle); + learnSoundHandle = p.play_stationary(soundPath, false); + } else { + speak_with_history("Sound not found.", true); + } + } + } +}