#include "sound_pool.nvgt" #include "virtual_dialogs.nvgt" #include "music.nvgt" // Audio // Increased pool size to 300 to prevent sound cutoffs and missing sounds sound_pool p(300); #include "src/constants.nvgt" #include "src/i18n.nvgt" #include "src/player.nvgt" #include "src/creature_base.nvgt" #include "src/creature_death.nvgt" #include "src/world_object_base.nvgt" #include "src/enemies/undead.nvgt" #include "src/enemies/bandit.nvgt" #include "src/enemies/ground_game.nvgt" #include "src/enemies/flying_creatures.nvgt" #include "src/world/world_drops.nvgt" #include "src/world/world_snares.nvgt" #include "src/world/world_fires.nvgt" #include "src/world/world_buildings.nvgt" #include "src/world/world_streams.nvgt" #include "src/world/mountains.nvgt" #include "src/world/barricade.nvgt" #include "src/world_state.nvgt" #include "libstorm-nvgt/ui.nvgt" #include "libstorm-nvgt/menu_helpers.nvgt" #include "libstorm-nvgt/menu_music.nvgt" #include "src/terrain_lookup.nvgt" #include "src/inventory.nvgt" #include "src/pet_system.nvgt" #include "src/quest_system.nvgt" #include "src/environment.nvgt" #include "src/combat.nvgt" #include "src/fylgja_system.nvgt" #include "src/save_system.nvgt" #include "src/base_system.nvgt" #include "src/time_system.nvgt" #include "src/fishing.nvgt" #include "src/weather.nvgt" #include "src/audio_utils.nvgt" #include "src/creature_audio.nvgt" #include "libstorm-nvgt/speech_history.nvgt" #include "src/text_reader_aliases.nvgt" #include "src/notify_compat.nvgt" #include "src/learn_sounds.nvgt" #include "src/intro.nvgt" #include "src/bosses/adventure_system.nvgt" const string MAIN_MENU_MUSIC_TRACK = "sounds/menu/Draugnorak.ogg; loop; f=1"; const int MAIN_MENU_MUSIC_FADE_IN_MS = 1; const int MAIN_MENU_MUSIC_FADE_OUT_MS = 800; music_manager mainMenuMusic; const int MAIN_MENU_NEW_GAME = 0; const int MAIN_MENU_LOAD_GAME = 1; const int MAIN_MENU_STORY = 2; const int MAIN_MENU_LANGUAGE = 3; const int MAIN_MENU_LEARN_SOUNDS = 4; const int MAIN_MENU_EXIT = 5; void start_main_menu_music() { menu_music_resume_or_play(mainMenuMusic, MAIN_MENU_MUSIC_TRACK, MAIN_MENU_MUSIC_FADE_IN_MS); } void stop_main_menu_music() { menu_music_pause(mainMenuMusic, MAIN_MENU_MUSIC_FADE_OUT_MS); } void choose_main_menu_language() { string[] languageCodes; string[] languageLabels; i18n_get_available_languages(languageCodes, languageLabels); int currentIndex = i18n_find_language_index(languageCodes, activeLanguageCode); if (currentIndex < 0) { currentIndex = i18n_find_language_index(languageCodes, I18N_DEFAULT_LANGUAGE_CODE); } if (currentIndex < 0) currentIndex = 0; int selectedIndex = i18n_run_language_selection_menu(languageCodes, languageLabels, currentIndex, true); if (selectedIndex < 0 || selectedIndex >= int(languageCodes.length())) return; string selectedCode = languageCodes[selectedIndex]; if (!i18n_load_language(selectedCode)) return; i18n_save_language_preference(selectedCode); string localizedWindowTitle = tr("system.ui.window_title"); show_window(localizedWindowTitle); ui_set_default_window_title(localizedWindowTitle); dictionary args; args.set("language", languageLabels[selectedIndex]); speak_with_history(trf("system.language.selected", args), true); } int run_main_menu() { start_main_menu_music(); speak_with_history(tr("system.main_menu.prompt"), true); int selection = 0; string load_label = has_save_game() ? i18n_text("Load Game") : i18n_text("Load Game (no saves found)"); string[] options; string[] entry_paths; int[] entry_types; // 0 = action, 1 = document int[] action_ids; options.insert_last(i18n_text("New Game")); entry_paths.insert_last(""); entry_types.insert_last(0); action_ids.insert_last(MAIN_MENU_NEW_GAME); options.insert_last(load_label); entry_paths.insert_last(""); entry_types.insert_last(0); action_ids.insert_last(MAIN_MENU_LOAD_GAME); options.insert_last(tr("system.menu.main.story")); entry_paths.insert_last(""); entry_types.insert_last(0); action_ids.insert_last(MAIN_MENU_STORY); options.insert_last(tr("system.menu.main.language")); entry_paths.insert_last(""); entry_types.insert_last(0); action_ids.insert_last(MAIN_MENU_LANGUAGE); options.insert_last(i18n_text("Learn Sounds")); entry_paths.insert_last(""); entry_types.insert_last(0); action_ids.insert_last(MAIN_MENU_LEARN_SOUNDS); string[] doc_labels; string[] doc_paths; int[] doc_types; add_doc_entries(doc_labels, doc_paths, doc_types); for (uint i = 0; i < doc_labels.length(); i++) { options.insert_last(doc_labels[i]); entry_paths.insert_last(doc_paths[i]); entry_types.insert_last(1); action_ids.insert_last(-1); } options.insert_last(i18n_text("Exit")); entry_paths.insert_last(""); entry_types.insert_last(0); action_ids.insert_last(MAIN_MENU_EXIT); int exit_action = MAIN_MENU_EXIT; speak_with_history(options[selection], true); while (true) { wait(5); mainMenuMusic.loop(); handle_global_volume_keys(); if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; if (selection >= options.length()) selection = 0; speak_with_history(options[selection], true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; if (selection < 0) selection = options.length() - 1; speak_with_history(options[selection], true); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); if (entry_types[selection] == 1) { stop_main_menu_music(); text_reader_file(entry_paths[selection], options[selection], true); start_main_menu_music(); speak_with_history(options[selection], true); continue; } stop_main_menu_music(); return action_ids[selection]; } if (key_pressed(KEY_ESCAPE)) { stop_main_menu_music(); return exit_action; } } return exit_action; } void run_game() { // Configure sound pool for better spatial audio p.volume_step = AUDIO_VOLUME_STEP / float(AUDIO_TILE_SCALE); // Default falloff in audio units p.pan_step = AUDIO_PAN_STEP; // Panning strength for scaled tile distances init_master_volume(); show_window("Draugnorak"); ui_set_default_window_title("Draugnorak"); i18n_init(); string localizedWindowTitle = tr("system.ui.window_title"); show_window(localizedWindowTitle); ui_set_default_window_title(localizedWindowTitle); init_flying_creature_configs(); init_item_registry(); init_pet_sounds(); init_search_pools(); while (true) { bool game_started = false; while (!game_started) { int selection = run_main_menu(); if (selection == MAIN_MENU_NEW_GAME) { if (!setup_new_character()) { continue; } start_new_game(); speak_with_history(tr("system.game.new_game_started"), true); game_started = true; } else if (selection == MAIN_MENU_LOAD_GAME) { if (!has_save_game()) { ui_info_box(tr("system.ui.window_title"), i18n_text("Load Game"), tr("system.load_game.no_saves_found")); continue; } string selectedFile; if (!select_save_file(selectedFile)) { continue; } if (load_game_state_from_file(selectedFile)) { speak_with_history(tr("system.game.loaded"), true); game_started = true; } else { string message = last_save_error; if (message == "") message = tr("system.load_game.unable_to_load"); ui_info_box(tr("system.ui.window_title"), i18n_text("Load Game"), message); } } else if (selection == MAIN_MENU_STORY) { run_story_dialog(); continue; } else if (selection == MAIN_MENU_LANGUAGE) { choose_main_menu_language(); continue; } else if (selection == MAIN_MENU_LEARN_SOUNDS) { run_learn_sounds_menu(); continue; } else { exit(); } } while (game_started) { wait(5); handle_global_volume_keys(); if (return_to_main_menu_requested) { game_paused = false; menuBackgroundUpdatesEnabled = true; p.resume_all(); reset_game_state(); return_to_main_menu_requested = false; break; } // Pause toggle if (key_pressed(KEY_BACK)) { game_paused = !game_paused; if (game_paused) { p.pause_all(); speak_with_history(tr("system.game.paused_prompt"), true); } else { p.resume_all(); // Restart all timers to prevent time from accumulating while paused restart_all_timers(); speak_with_history(tr("system.game.resumed"), true); } continue; } // Skip all game logic while paused if (game_paused) { continue; } check_pet_call_key(); if (key_pressed(KEY_ESCAPE)) { int really_exit = ui_question("", tr("system.exit.confirm")); if (really_exit == 1) { return_to_main_menu_requested = true; continue; } } // Time & Environment updates update_time(); update_crossfade(); update_weather(); update_environment(); update_snares(); update_streams(); update_fires(); update_zombies(); update_bandits(); update_boars(); update_flying_creatures(); update_weapon_range_audio_all(); update_world_drops(); update_pets(); update_blessings(); update_notifications(); // Fire damage check (only if not jumping) WorldFire @fire_on_tile = get_fire_at(x); if (fire_on_tile != null && !jumping && fire_damage_timer.elapsed > 1000) { player_health--; fire_damage_timer.restart(); dictionary burningArgs; burningArgs.set("health", player_health); speak_with_history(trf("system.combat.burning", burningArgs), true); } // Healing in base area if (x <= BASE_END && player_health < max_health) { WorldHerbGarden @herb_garden = get_herb_garden_at_base(); int heal_interval = (herb_garden != null) ? 30000 : 150000; // 30 seconds with garden, 2.5 minutes without if (healing_timer.elapsed > heal_interval) { player_health++; healing_timer.restart(); dictionary healthArgs; healthArgs.set("health", player_health); speak_with_history(trf("system.combat.health_status", healthArgs), true); } } // Death check if (player_health <= 0) { if (!try_consume_heal_scroll()) { speak_with_history(tr("system.combat.you_died"), true); wait(2000); return_to_main_menu_requested = true; continue; } } if (fylgjaCharging) { update_fylgja_charge(); continue; } // Inventory & Actions check_inventory_keys(x); check_action_menu(x); check_adventure_menu(x); check_crafting_menu(x, BASE_END); check_altar_menu(x); check_equipment_menu(); check_quest_menu(); check_fylgja_menu(); check_quick_slot_keys(); check_time_input(); check_notification_keys(); check_speech_history_keys(); // Health Key if (key_pressed(KEY_H)) { speak_with_history(get_health_report(), true); } // Coordinates Key if (key_pressed(KEY_X)) { string direction_label = (facing == 1) ? tr("system.direction.east") : tr("system.direction.west"); string terrain = i18n_translate_fragment_value(get_terrain_at_position(x)); if (get_mountain_at(x) !is null) { dictionary terrainArgs; terrainArgs.set("terrain", terrain); terrain = trf("system.character.report.terrain_mountains", terrainArgs); } dictionary positionArgs; positionArgs.set("direction", direction_label); positionArgs.set("x", x); positionArgs.set("y", y); positionArgs.set("terrain", terrain); speak_with_history(trf("system.character.report.position", positionArgs), true); } // Base Info Key (base only) if (key_pressed(KEY_B)) { run_base_info_menu(); } // Climbing and Falling Updates update_climbing(); update_falling(); update_rope_climbing(); check_rope_climb_fall(); update_mountains(); // Down arrow to climb down from tree or start rope climb down if (key_pressed(KEY_DOWN)) { // Check for pending rope climb (going down) if (pending_rope_climb_x != -1 && !rope_climbing && !climbing && !falling && !jumping) { int elevation_change = pending_rope_climb_elevation - y; if (elevation_change < 0) { start_rope_climb(false, pending_rope_climb_x, pending_rope_climb_elevation); pending_rope_climb_x = -1; } } // Tree climbing down else { Tree @tree = get_tree_at(x); if (tree != null && !tree.is_chopped && y > 0 && !jumping && !climbing && !falling && !rope_climbing) { climb_down_tree(); } } } // Jumping Logic if (key_pressed(KEY_UP)) { // Check for pending rope climb (going up) if (pending_rope_climb_x != -1 && !rope_climbing && !climbing && !falling && !jumping) { int elevation_change = pending_rope_climb_elevation - y; if (elevation_change > 0) { start_rope_climb(true, pending_rope_climb_x, pending_rope_climb_elevation); pending_rope_climb_x = -1; } } else if (!jumping && !climbing && !falling && !rope_climbing) { // Get ground elevation at current position int ground_elevation = get_mountain_elevation_at(x); // Check if on tree tile Tree @tree = get_tree_at(x); if (tree != null && !tree.is_chopped && y == ground_elevation) { // Start climbing the tree start_climbing_tree(x); } else if (y == ground_elevation) { // Normal jump (only if on the ground) p.play_stationary("sounds/jump.ogg", false); jumping = true; jumptimer.restart(); } } } if (jumping && jumptimer.elapsed > 850) { jumping = false; // Reset y to ground elevation after jump y = get_mountain_elevation_at(x); play_land_sound(x, BASE_END, GRASS_END); // Check for snare on landing? check_snare_collision(x); } // Set y to 3 above ground during jump if (jumping && y == get_mountain_elevation_at(x)) { y = get_mountain_elevation_at(x) + 3; } movetime = jumping ? jump_speed : walk_speed; bool left_active = key_down(KEY_LEFT); bool right_active = key_down(KEY_RIGHT); // Movement Logic if (key_pressed(KEY_LEFT) && facing != 0 && !climbing && !falling && !rope_climbing) { facing = 0; speak_with_history(tr("system.direction.west"), true); walktimer.restart(); // Cancel pending rope climb when changing direction pending_rope_climb_x = -1; } if (key_pressed(KEY_RIGHT) && facing != 1 && !climbing && !falling && !rope_climbing) { facing = 1; speak_with_history(tr("system.direction.east"), true); walktimer.restart(); // Cancel pending rope climb when changing direction pending_rope_climb_x = -1; } if (walktimer.elapsed > movetime) { int old_x = x; // Check if trying to move left/right while in a tree. Tree @current_tree = get_tree_at(x); int ground_elevation = get_mountain_elevation_at(x); if ((left_active || right_active) && y > ground_elevation && !jumping && !falling && !rope_climbing && current_tree !is null) { // Fall out of tree climbing = false; start_falling(); } if (left_active && x > 0 && !climbing && !falling && !rope_climbing) { facing = 0; int target_x = x - 1; // Check mountain movement and deep water if (can_move_mountain(x, target_x) && can_enter_stream_tile(target_x)) { x = target_x; // Always update elevation to match ground (0 if not in mountain) int new_elevation = get_mountain_elevation_at(x); if (new_elevation != y) { y = new_elevation; } walktimer.restart(); if (!jumping) { play_footstep(x, BASE_END, GRASS_END); check_snare_collision(x); } } } else if (right_active && x < MAP_SIZE - 1 && !climbing && !falling && !rope_climbing) { facing = 1; int target_x = x + 1; // Check mountain movement and deep water if (can_move_mountain(x, target_x) && can_enter_stream_tile(target_x)) { x = target_x; // Always update elevation to match ground (0 if not in mountain) int new_elevation = get_mountain_elevation_at(x); if (new_elevation != y) { y = new_elevation; } walktimer.restart(); if (!jumping) { play_footstep(x, BASE_END, GRASS_END); check_snare_collision(x); } } } } // Reset search timer if shift is used with other keys if ((key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT))) { if (key_pressed(KEY_COMMA) || key_pressed(KEY_PERIOD)) { searching = false; search_delay_timer.restart(); } } // Searching Logic bool shift_down = (key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT)); bool search_key_down = shift_down || key_down(KEY_SLASH) || key_down(KEY_Z); if (!search_key_down && !searching) { search_timer.restart(); } // Apply rune gathering bonus to search time int search_time = apply_rune_gather_bonus(2000); if (search_key_down && search_timer.elapsed > search_time && !searching) { searching = true; search_delay_timer.restart(); } // Complete search after delay if (searching && search_delay_timer.elapsed >= 1000) { searching = false; search_timer.restart(); perform_search(x); } update_fishing(); update_bow_shot(); bool ctrl_down = (key_down(KEY_LCTRL) || key_down(KEY_RCTRL)); // Bow draw detection if (bow_equipped) { if (ctrl_down && !bow_drawing) { if (get_personal_count(ITEM_ARROWS) > 0) { bow_drawing = true; bow_draw_timer.restart(); p.play_stationary("sounds/weapons/bow_draw.ogg", false); } else { speak_ammo_blocked("No arrows."); } } if (bow_drawing && !ctrl_down) { release_bow_attack(x); bow_drawing = false; } } if (!bow_equipped && bow_drawing) { bow_drawing = false; } // Sling charge detection if (!bow_equipped && sling_equipped && ctrl_down && !sling_charging) { if (get_personal_count(ITEM_STONES) > 0) { sling_charging = true; sling_charge_timer.restart(); sling_sound_handle = p.play_stationary("sounds/weapons/sling_swing.ogg", true); last_sling_stage = -1; } else { speak_ammo_blocked("No stones."); } } // Update sling charge state while holding if (sling_charging && ctrl_down) { update_sling_charge(); } // Sling release detection if (sling_charging && !ctrl_down) { release_sling_attack(x); sling_charging = false; if (sling_sound_handle != -1) { p.destroy_sound(sling_sound_handle); sling_sound_handle = -1; } } // Non-sling weapon attacks (existing pattern) if (!bow_equipped && !bow_drawing && !sling_equipped && !sling_charging) { int attack_cooldown = 1000; if (spear_equipped) attack_cooldown = 800; if (axe_equipped) attack_cooldown = 1600; if (!fishing_pole_equipped && ctrl_down && attack_timer.elapsed > attack_cooldown) { attack_timer.restart(); perform_attack(x); } } // Audio Listener Update p.update_listener_1d(x); } } } void main() { try { run_game(); } catch { log_unhandled_exception("main"); ui_info_box("Draugnorak", "Unhandled exception", "A crash log was written to your save data folder."); } }