Wire Draugnorak to reusable libstorm modules

Replace local ui/speech/text reader/notification modules with libstorm-nvgt integrations, keeping game-specific terrain lookup local via a new module.\n\nAdd compatibility layers for notifications and text_reader callsites, switch menu prefix filtering to shared menu helpers, and update the libstorm-nvgt submodule to include the learn_sounds select-sound toggle.
This commit is contained in:
Storm Dragon
2026-02-16 20:25:43 -05:00
parent 805c63f990
commit 4d5bb7c2fa
11 changed files with 119 additions and 665 deletions
+33 -249
View File
@@ -1,268 +1,52 @@
// Learn sounds menu
#include "libstorm-nvgt/learn_sounds.nvgt"
#include "libstorm-nvgt/docs_browser.nvgt"
#include "excluded_sounds.nvgt"
// 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/menu/",
"sounds/nature/",
"sounds/pets/"
};
bool learnSoundsConfigured = false;
bool docsBrowserConfigured = false;
// 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);
void learn_sounds_bridge_speak(const string &in message, bool interrupt) {
speak_with_history(message, interrupt);
}
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);
void learn_sounds_bridge_tick() {
handle_global_volume_keys();
}
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;
void configure_docs_browser_if_needed() {
if (docsBrowserConfigured) return;
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;
}
docs_browser_set_speak_callback(learn_sounds_bridge_speak);
docs_browser_set_tick_callback(learn_sounds_bridge_tick);
docs_browser_set_docs_dir("files");
docs_browser_set_menu_sound_dir("sounds/menu");
docs_browser_set_wrap(true);
docs_browser_reset_default_extensions();
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();
}
void append_unique_case_insensitive(string[]@ items, const string&in value) {
string lowerValue = value.lower();
for (uint i = 0; i < items.length(); i++) {
if (items[i].lower() == lowerValue) return;
}
items.insert_last(value);
}
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;
docsBrowserConfigured = true;
}
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++) {
append_unique_case_insensitive(docFiles, mdFiles[i]);
}
}
string[]@ mdCapsFiles = find_files("files/*.MD");
if (@mdCapsFiles !is null) {
for (uint i = 0; i < mdCapsFiles.length(); i++) {
append_unique_case_insensitive(docFiles, 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);
}
configure_docs_browser_if_needed();
docs_browser_add_entries(labels, paths, types, 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);
}
void configure_learn_sounds_if_needed() {
if (learnSoundsConfigured) return;
for (uint i = 0; i < soundFiles.length(); i++) {
string soundPath = normalize_path(soundFiles[i]);
if (should_skip_sound_path(soundPath)) continue;
learn_sounds_set_speak_callback(learn_sounds_bridge_speak);
learn_sounds_set_tick_callback(learn_sounds_bridge_tick);
learn_sounds_set_root_dir("sounds");
learn_sounds_set_menu_sound_dir("sounds/menu");
learn_sounds_set_wrap(true);
learn_sounds_set_play_select_sound(false);
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);
}
configure_project_learn_sounds();
learnSoundsConfigured = true;
}
void run_learn_sounds_menu() {
speak_with_history("Learn sounds.", true);
string[] labels;
string[] entryPaths;
int[] entryTypes; // 0 = sound
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);
handle_global_volume_keys();
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);
}
}
}
configure_learn_sounds_if_needed();
learn_sounds_run_menu();
}
+3 -63
View File
@@ -105,73 +105,13 @@ string get_base_fire_status() {
}
string get_menu_filter_letter() {
if (key_pressed(KEY_A)) return "a";
if (key_pressed(KEY_B)) return "b";
if (key_pressed(KEY_C)) return "c";
if (key_pressed(KEY_D)) return "d";
if (key_pressed(KEY_E)) return "e";
if (key_pressed(KEY_F)) return "f";
if (key_pressed(KEY_G)) return "g";
if (key_pressed(KEY_H)) return "h";
if (key_pressed(KEY_I)) return "i";
if (key_pressed(KEY_J)) return "j";
if (key_pressed(KEY_K)) return "k";
if (key_pressed(KEY_L)) return "l";
if (key_pressed(KEY_M)) return "m";
if (key_pressed(KEY_N)) return "n";
if (key_pressed(KEY_O)) return "o";
if (key_pressed(KEY_P)) return "p";
if (key_pressed(KEY_Q)) return "q";
if (key_pressed(KEY_R)) return "r";
if (key_pressed(KEY_S)) return "s";
if (key_pressed(KEY_T)) return "t";
if (key_pressed(KEY_U)) return "u";
if (key_pressed(KEY_V)) return "v";
if (key_pressed(KEY_W)) return "w";
if (key_pressed(KEY_X)) return "x";
if (key_pressed(KEY_Y)) return "y";
if (key_pressed(KEY_Z)) return "z";
return "";
return menu_get_filter_letter();
}
void apply_menu_filter(const string &in filter_text, const string[]@ options, int[]@ filtered_indices, string[]@ filtered_options) {
filtered_indices.resize(0);
filtered_options.resize(0);
string filter_lower = filter_text.lower();
for (uint i = 0; i < options.length(); i++) {
if (filter_lower.length() == 0) {
filtered_indices.insert_last(i);
filtered_options.insert_last(options[i]);
continue;
}
string option_lower = options[i].lower();
if (option_lower.length() >= filter_lower.length() && option_lower.substr(0, filter_lower.length()) == filter_lower) {
filtered_indices.insert_last(i);
filtered_options.insert_last(options[i]);
}
}
menu_apply_prefix_filter(filter_text, options, filtered_indices, filtered_options);
}
bool update_menu_filter_state(string &inout filter_text, const string[]@ options, int[]@ filtered_indices, string[]@ filtered_options, int &inout selection) {
bool filter_changed = false;
if (key_pressed(KEY_BACK) && filter_text.length() > 0) {
filter_text = filter_text.substr(0, filter_text.length() - 1);
filter_changed = true;
}
string filter_letter = get_menu_filter_letter();
if (filter_letter != "") {
filter_text += filter_letter;
filter_changed = true;
}
if (filter_changed) {
apply_menu_filter(filter_text, options, filtered_indices, filtered_options);
if (selection >= int(filtered_options.length())) selection = 0;
}
return filter_changed;
return menu_update_prefix_filter(filter_text, options, filtered_indices, filtered_options, selection);
}
-108
View File
@@ -1,108 +0,0 @@
// Notification System
string[] notification_history;
const int MAX_NOTIFICATIONS = 10;
const int NOTIFICATION_DELAY = 3000; // 3 seconds between notifications
int current_notification_index = -1;
string[] notification_queue;
timer notification_timer;
bool notification_active = false;
int notification_sound_handle = -1;
void notify(string message) {
// Add to queue (don't play yet)
notification_queue.insert_last(message);
// Add to history immediately so it appears in history even if queued
notification_history.insert_last(message);
// Keep only last 10 notifications
if (notification_history.length() > MAX_NOTIFICATIONS) {
notification_history.remove_at(0);
}
// Reset index to most recent
current_notification_index = notification_history.length() - 1;
}
void update_notifications() {
if (notification_queue.length() == 0) {
if (notification_active && notification_timer.elapsed >= NOTIFICATION_DELAY) {
notification_active = false;
}
notification_sound_handle = -1;
return;
}
// If a notification is currently active, wait for delay
if (notification_active && notification_timer.elapsed < NOTIFICATION_DELAY) {
return;
}
// If we're waiting for the notification sound to finish playing
if (notification_sound_handle != -1) {
// Check if sound is still playing
if (p.sound_is_active(notification_sound_handle)) {
return; // Still playing, wait
}
// Sound finished, now speak
speak_with_history(notification_queue[0], true);
notification_queue.remove_at(0);
notification_sound_handle = -1;
// Start timer for next notification
notification_timer.restart();
notification_active = true;
return;
}
// Play next notification sound (don't speak yet)
notification_sound_handle = p.play_stationary("sounds/notify.ogg", false);
}
void check_notification_keys() {
// [ for previous notification (older) with position
if (key_pressed(KEY_LEFTBRACKET)) {
if (notification_history.length() == 0) {
speak_with_history("No notifications.", true);
return;
}
current_notification_index--;
if (current_notification_index < 0) {
current_notification_index = 0;
speak_with_history("Oldest notification. " + notification_history[current_notification_index], true);
return;
}
int position = current_notification_index + 1;
speak_with_history(notification_history[current_notification_index] + " " + position + " of " + notification_history.length(), true);
return;
}
// ] for next notification (newer) with position
if (key_pressed(KEY_RIGHTBRACKET)) {
if (notification_history.length() == 0) {
speak_with_history("No notifications.", true);
return;
}
current_notification_index++;
if (current_notification_index >= notification_history.length()) {
current_notification_index = notification_history.length() - 1;
speak_with_history("Newest notification. " + notification_history[current_notification_index], true);
return;
}
int position = current_notification_index + 1;
speak_with_history(notification_history[current_notification_index] + " " + position + " of " + notification_history.length(), true);
return;
}
// \ for most recent notification (without position)
if (key_pressed(KEY_BACKSLASH)) {
if (notification_history.length() == 0) {
speak_with_history("No notifications.", true);
return;
}
current_notification_index = notification_history.length() - 1;
speak_with_history(notification_history[current_notification_index], true);
}
}
+36
View File
@@ -0,0 +1,36 @@
#include "libstorm-nvgt/notifications.nvgt"
const int NOTIFICATION_HISTORY_LIMIT = 10;
const int NOTIFICATION_DELAY_MS = 3000;
const string NOTIFICATION_SOUND_PATH = "sounds/notify";
bool notificationsCompatConfigured = false;
void notify_compat_speak(const string &in message, bool interrupt) {
speak_with_history(message, interrupt);
}
void configure_notification_compat_if_needed() {
if (notificationsCompatConfigured) return;
notifications_set_max_history(NOTIFICATION_HISTORY_LIMIT);
notifications_set_delay_ms(NOTIFICATION_DELAY_MS);
notifications_set_sound_path(NOTIFICATION_SOUND_PATH);
notifications_set_speak_callback(notify_compat_speak);
notificationsCompatConfigured = true;
}
void notify(string message) {
configure_notification_compat_if_needed();
notifications_enqueue(message);
}
void update_notifications() {
configure_notification_compat_if_needed();
notifications_update();
}
void check_notification_keys() {
configure_notification_compat_if_needed();
notifications_check_keys();
}
-72
View File
@@ -1,72 +0,0 @@
// Speech History System
// Tracks last 10 unique screen reader announcements for navigation with comma/period
string[] speech_history;
const int MAX_SPEECH_HISTORY = 10;
int current_speech_index = -1;
// Main speak function - wrapper around screen_reader_speak with history tracking
void speak_with_history(string message, bool interrupt) {
// Only add to history if not already present (no duplicates)
bool already_exists = false;
for (uint i = 0; i < speech_history.length(); i++) {
if (speech_history[i] == message) {
already_exists = true;
break;
}
}
// Add to history if not a duplicate
if (!already_exists) {
speech_history.insert_last(message);
// Keep only last 10 messages
if (speech_history.length() > MAX_SPEECH_HISTORY) {
speech_history.remove_at(0);
}
// Reset index to most recent
current_speech_index = speech_history.length() - 1;
}
// Call the built-in screen_reader_speak function
screen_reader_speak(message, interrupt);
}
void check_speech_history_keys() {
// , (comma) for previous speech message (older)
if (key_pressed(KEY_COMMA)) {
if (speech_history.length() == 0) {
screen_reader_speak("No speech history.", true);
return;
}
current_speech_index--;
if (current_speech_index < 0) {
current_speech_index = 0;
screen_reader_speak("Oldest message. " + speech_history[current_speech_index], true);
return;
}
int position = current_speech_index + 1;
screen_reader_speak(speech_history[current_speech_index] + " " + position + " of " + speech_history.length(), true);
return;
}
// . (period) for next speech message (newer)
if (key_pressed(KEY_PERIOD)) {
if (speech_history.length() == 0) {
screen_reader_speak("No speech history.", true);
return;
}
current_speech_index++;
if (current_speech_index >= speech_history.length()) {
current_speech_index = speech_history.length() - 1;
screen_reader_speak("Newest message. " + speech_history[current_speech_index], true);
return;
}
int position = current_speech_index + 1;
screen_reader_speak(speech_history[current_speech_index] + " " + position + " of " + speech_history.length(), true);
return;
}
}
+26
View File
@@ -0,0 +1,26 @@
// Game-specific terrain lookup used by movement, menus, and reports.
string get_terrain_at_position(int posX) {
if (is_position_in_water(posX) || is_mountain_stream_at(posX)) {
return "water";
}
MountainRange@ mountain = get_mountain_at(posX);
if (mountain !is null) {
return mountain.get_terrain_at(posX);
}
if (posX <= BASE_END) return "wood";
if (posX <= GRASS_END) return "grass";
if (posX <= GRAVEL_END) return "gravel";
int index = posX - expanded_area_start;
if (index >= 0 && index < int(expanded_terrain_types.length())) {
string terrain = expanded_terrain_types[index];
if (terrain.find("mountain:") == 0) {
terrain = terrain.substr(9);
}
return terrain;
}
return "unknown";
}
-115
View File
@@ -1,115 +0,0 @@
// text_reader.nvgt - Simple text document reader/editor using NVGT's audio_form
// Provides accessible navigation through text documents with optional editing
#include "form.nvgt"
// Opens a text reader/editor window with string content
// Parameters:
// content: The text content to display (can be file contents or direct string)
// title: Window title (default: "Text Reader")
// readonly: If true, text cannot be edited (default: true)
// Returns: The modified text if readonly=false and user presses OK, empty string if canceled or readonly=true
string text_reader(string content, string title = "Text Reader", bool readonly = true) {
audio_form f;
f.create_window(title, false, true);
// Create the multiline input box
// In readonly mode, it's still navigable with arrows/home/end/etc
// In edit mode, user can modify text
int text_control = f.create_input_box(
(readonly ? "Document (read only)" : "Document (editable)"),
content,
"", // no password mask
0, // no max length
readonly,
true, // multiline = true
true // multiline_enter = true (Ctrl+Enter for newlines)
);
int ok_button = -1;
int close_button = -1;
if (readonly) {
// In readonly mode, just have a Close button
close_button = f.create_button("&Close", true, true);
} else {
// In edit mode, have OK and Cancel buttons
ok_button = f.create_button("&OK", true);
close_button = f.create_button("&Cancel", false, true);
}
f.focus(text_control);
// Monitor loop
while (true) {
f.monitor();
wait(5);
handle_global_volume_keys();
// Check if user pressed OK (edit mode only)
if (!readonly && ok_button != -1 && f.is_pressed(ok_button)) {
return f.get_text(text_control);
}
// Check if user pressed Close/Cancel
if (close_button != -1 && f.is_pressed(close_button)) {
return "";
}
// Check for Escape key as alternative close method
if (key_pressed(KEY_ESCAPE)) {
return "";
}
}
return "";
}
// Opens a text reader/editor window with an array of lines
// Parameters:
// lines: Array of text lines to display
// title: Window title (default: "Text Reader")
// readonly: If true, text cannot be edited (default: true)
// Returns: The modified text if readonly=false and user presses OK, empty string if canceled or readonly=true
string text_reader_lines(string[] lines, string title = "Text Reader", bool readonly = true) {
string content = join(lines, "\n");
return text_reader(content, title, readonly);
}
// Convenience function to read a file and display it in the text reader
// Parameters:
// file_path: Path to the file to read
// title: Window title (default: uses file_path as title)
// readonly: If true, text cannot be edited (default: true)
// Returns: The modified text if readonly=false and user saves, empty string otherwise
string text_reader_file(string file_path, string title = "", bool readonly = true) {
file f;
if (!f.open(file_path, "rb")) {
screen_reader_speak("Failed to open file: " + file_path, true);
return "";
}
string content = f.read();
f.close();
// Use file_path as title if no custom title provided
if (title == "") {
title = file_path;
}
string result = text_reader(content, title, readonly);
// If in edit mode and user pressed OK, save the file
if (!readonly && result != "") {
if (f.open(file_path, "wb")) {
f.write(result);
f.close();
screen_reader_speak("File saved successfully", true);
return result;
} else {
screen_reader_speak("Failed to save file", true);
}
}
return result;
}
+13
View File
@@ -0,0 +1,13 @@
// Compatibility aliases that keep legacy text_reader* callsites unchanged.
// file_viewer* is provided by libstorm-nvgt/docs_browser.nvgt.
string text_reader(string content, string title = "Text Reader", bool readOnly = true) {
return file_viewer(content, title, readOnly);
}
string text_reader_lines(string[] lines, string title = "Text Reader", bool readOnly = true) {
return file_viewer_lines(lines, title, readOnly);
}
string text_reader_file(string filePath, string title = "", bool readOnly = true) {
return file_viewer_file(filePath, title, readOnly);
}
-53
View File
@@ -1,53 +0,0 @@
// UI helpers
string get_terrain_at_position(int pos_x) {
// Check for water first (streams in expanded areas or mountain streams)
if (is_position_in_water(pos_x) || is_mountain_stream_at(pos_x)) {
return "water";
}
// Check mountain terrain
MountainRange@ mountain = get_mountain_at(pos_x);
if (mountain !is null) {
return mountain.get_terrain_at(pos_x);
}
// Base area
if (pos_x <= BASE_END) return "wood";
// Grass area
if (pos_x <= GRASS_END) return "grass";
// Gravel area
if (pos_x <= GRAVEL_END) return "gravel";
// Expanded areas
int index = pos_x - expanded_area_start;
if (index >= 0 && index < int(expanded_terrain_types.length())) {
string terrain = expanded_terrain_types[index];
// Handle "mountain:terrain" format from older saves
if (terrain.find("mountain:") == 0) {
terrain = terrain.substr(9);
}
return terrain;
}
return "unknown";
}
string ui_input_box(const string title, const string prompt, const string default_value) {
string result = virtual_input_box(prompt, prompt, default_value);
show_window("Draugnorak");
return result;
}
int ui_question(const string title, const string prompt) {
// Put the prompt in both title (for screen reader) and message (for dialog to work)
int result = virtual_question(prompt, prompt);
show_window("Draugnorak");
return result;
}
void ui_info_box(const string title, const string heading, const string message) {
virtual_info_box(title, heading, message);
show_window("Draugnorak");
}