From af6b3e4e3e6616c1e64ba4e7b51dc3b1a2cb0472 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 12 Feb 2026 03:30:38 -0500 Subject: [PATCH] A few bugs fixed. Very very rough draft of pet system. Hopefully it works, maybe it doesn't. --- draugnorak.nvgt | 16 +- sounds/enemies/bandit3.ogg | 3 + sounds/enemies/bandit4.ogg | 3 + sounds/enemies/bandit_female_dies.ogg | 3 + sounds/pets/call_pet.ogg | 3 + sounds/pets/hawk.ogg | 3 + sounds/pets/wolf.ogg | 3 + src/audio_utils.nvgt | 36 ++ src/bosses/adventure_system.nvgt | 1 + src/bosses/bandit_hideout.nvgt | 4 + src/bosses/unicorn/unicorn_boss.nvgt | 4 + src/constants.nvgt | 21 + src/crafting/craft_barricade.nvgt | 3 +- src/crafting/craft_buildings.nvgt | 3 +- src/crafting/craft_clothing.nvgt | 3 +- src/crafting/craft_materials.nvgt | 3 +- src/crafting/craft_runes.nvgt | 6 +- src/crafting/craft_tools.nvgt | 3 +- src/crafting/craft_weapons.nvgt | 3 +- src/crafting/crafting_core.nvgt | 3 +- src/creature_audio.nvgt | 7 + src/environment.nvgt | 1 + src/inventory_menus.nvgt | 2 +- src/learn_sounds.nvgt | 1 + src/menus/base_info.nvgt | 7 +- src/menus/character_info.nvgt | 89 +++- src/menus/menu_utils.nvgt | 2 + src/pet_system.nvgt | 647 +++++++++++++++++++++++ src/quest_system.nvgt | 1 + src/quests/bat_invasion_game.nvgt | 1 + src/quests/catch_the_boomerang_game.nvgt | 2 + src/quests/enchanted_melody_game.nvgt | 3 + src/quests/escape_from_hel_game.nvgt | 3 + src/quests/skeletal_bard_game.nvgt | 1 + src/save_system.nvgt | 46 +- src/text_reader.nvgt | 1 + src/time_system.nvgt | 38 +- 37 files changed, 936 insertions(+), 43 deletions(-) create mode 100644 sounds/enemies/bandit3.ogg create mode 100644 sounds/enemies/bandit4.ogg create mode 100644 sounds/enemies/bandit_female_dies.ogg create mode 100644 sounds/pets/call_pet.ogg create mode 100644 sounds/pets/hawk.ogg create mode 100644 sounds/pets/wolf.ogg create mode 100644 src/pet_system.nvgt diff --git a/draugnorak.nvgt b/draugnorak.nvgt index a9d7318..6f8b08c 100755 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -25,6 +25,7 @@ sound_pool p(300); #include "src/world_state.nvgt" #include "src/ui.nvgt" #include "src/inventory.nvgt" +#include "src/pet_system.nvgt" #include "src/quest_system.nvgt" #include "src/environment.nvgt" #include "src/combat.nvgt" @@ -115,6 +116,7 @@ int run_main_menu() { while(true) { wait(5); mainMenuMusic.loop(); + handle_global_volume_keys(); if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); @@ -157,10 +159,12 @@ 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"); init_flying_creature_configs(); init_item_registry(); + init_pet_sounds(); init_search_pools(); while (true) { @@ -200,10 +204,11 @@ void run_game() } } - while (game_started) { - wait(5); + while (game_started) { + wait(5); + handle_global_volume_keys(); - if (return_to_main_menu_requested) { + if (return_to_main_menu_requested) { game_paused = false; menuBackgroundUpdatesEnabled = true; p.resume_all(); @@ -232,7 +237,9 @@ void run_game() if (game_paused) { continue; } - + + check_pet_call_key(); + if(key_pressed(KEY_ESCAPE)) { int really_exit = ui_question("", "Really exit?"); @@ -256,6 +263,7 @@ void run_game() update_flying_creatures(); update_weapon_range_audio_all(); update_world_drops(); + update_pets(); update_blessings(); update_notifications(); diff --git a/sounds/enemies/bandit3.ogg b/sounds/enemies/bandit3.ogg new file mode 100644 index 0000000..504c44f --- /dev/null +++ b/sounds/enemies/bandit3.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df337c4a3de82eeaeac6a4373a385e5b77b0b9789e7533dcfa62ad77f2a5f347 +size 16719 diff --git a/sounds/enemies/bandit4.ogg b/sounds/enemies/bandit4.ogg new file mode 100644 index 0000000..27fd54e --- /dev/null +++ b/sounds/enemies/bandit4.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b29c61d1ac580686bd8f3f2ff896ae1d5126dfecb7fd97c49473138c2adf9705 +size 18204 diff --git a/sounds/enemies/bandit_female_dies.ogg b/sounds/enemies/bandit_female_dies.ogg new file mode 100644 index 0000000..04d4541 --- /dev/null +++ b/sounds/enemies/bandit_female_dies.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d74bfacb2363c92475d3d56531efb3e18b3eff15cacb7b414966bc2fd51632cf +size 17141 diff --git a/sounds/pets/call_pet.ogg b/sounds/pets/call_pet.ogg new file mode 100644 index 0000000..9fb3af0 --- /dev/null +++ b/sounds/pets/call_pet.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2cf45d7598f8a1e0930b9b3806895945c1bd39a81cce0fe0c6f398e1b859a59 +size 10492 diff --git a/sounds/pets/hawk.ogg b/sounds/pets/hawk.ogg new file mode 100644 index 0000000..4778036 --- /dev/null +++ b/sounds/pets/hawk.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e66b3420c358d18612538c98c3f70e0424f3f821917a2fef90d1c0a6faaa607d +size 30007 diff --git a/sounds/pets/wolf.ogg b/sounds/pets/wolf.ogg new file mode 100644 index 0000000..47bdbb4 --- /dev/null +++ b/sounds/pets/wolf.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0186ac03485fe7503cd316914a75cfcb4b7441ba3ca49fc7ab7155cd991b7af +size 28216 diff --git a/src/audio_utils.nvgt b/src/audio_utils.nvgt index c088b9d..cfe8997 100644 --- a/src/audio_utils.nvgt +++ b/src/audio_utils.nvgt @@ -205,3 +205,39 @@ void safe_destroy_sound(int &inout handle) handle = -1; } } + +float master_volume_db = MASTER_VOLUME_MAX_DB; + +void init_master_volume() { + master_volume_db = MASTER_VOLUME_MAX_DB; + sound_master_volume = master_volume_db; +} + +void set_game_master_volume_db(float volume_db, bool announce = true) { + float clamped = volume_db; + if (clamped > MASTER_VOLUME_MAX_DB) clamped = MASTER_VOLUME_MAX_DB; + if (clamped < MASTER_VOLUME_MIN_DB) clamped = MASTER_VOLUME_MIN_DB; + + if (clamped == master_volume_db) return; + + master_volume_db = clamped; + sound_master_volume = master_volume_db; + + if (announce) { + float range = MASTER_VOLUME_MAX_DB - MASTER_VOLUME_MIN_DB; + float normalized = (master_volume_db - MASTER_VOLUME_MIN_DB) / range; + int volumePercent = int(normalized * 100.0f + 0.5f); + if (volumePercent < 0) volumePercent = 0; + if (volumePercent > 100) volumePercent = 100; + screen_reader_speak("Volume " + volumePercent + ".", true); + } +} + +void handle_global_volume_keys() { + if (key_pressed(KEY_PAGEDOWN)) { + set_game_master_volume_db(master_volume_db - MASTER_VOLUME_STEP_DB); + } + if (key_pressed(KEY_PAGEUP)) { + set_game_master_volume_db(master_volume_db + MASTER_VOLUME_STEP_DB); + } +} diff --git a/src/bosses/adventure_system.nvgt b/src/bosses/adventure_system.nvgt index 1e0dec9..3432d67 100644 --- a/src/bosses/adventure_system.nvgt +++ b/src/bosses/adventure_system.nvgt @@ -52,6 +52,7 @@ void run_adventure_menu(int player_x) { while (true) { wait(5); + handle_global_volume_keys(); if (key_pressed(KEY_ESCAPE)) { speak_with_history("Closed.", true); diff --git a/src/bosses/bandit_hideout.nvgt b/src/bosses/bandit_hideout.nvgt index ea84926..d359422 100644 --- a/src/bosses/bandit_hideout.nvgt +++ b/src/bosses/bandit_hideout.nvgt @@ -261,6 +261,7 @@ void run_bandit_hideout_adventure() { bool adventurePaused = false; while (true) { wait(5); + handle_global_volume_keys(); if (key_pressed(KEY_BACK)) { adventurePaused = !adventurePaused; @@ -280,6 +281,8 @@ void run_bandit_hideout_adventure() { continue; } + check_pet_call_key(); + if (key_pressed(KEY_ESCAPE)) { cleanup_bandit_hideout_adventure(); speak_with_history("You flee the hideout.", true); @@ -760,4 +763,5 @@ void give_bandit_hideout_rewards() { } text_reader_lines(rewards, "Bandit's Hideout", true); + attempt_pet_offer_from_adventure(); } diff --git a/src/bosses/unicorn/unicorn_boss.nvgt b/src/bosses/unicorn/unicorn_boss.nvgt index 0c4badf..ac6ce81 100644 --- a/src/bosses/unicorn/unicorn_boss.nvgt +++ b/src/bosses/unicorn/unicorn_boss.nvgt @@ -122,6 +122,7 @@ void run_unicorn_adventure() { bool adventurePaused = false; while (true) { wait(5); + handle_global_volume_keys(); if (key_pressed(KEY_BACK)) { adventurePaused = !adventurePaused; @@ -141,6 +142,8 @@ void run_unicorn_adventure() { continue; } + check_pet_call_key(); + // Input Handling if (key_pressed(KEY_ESCAPE)) { cleanup_unicorn_adventure(); @@ -670,4 +673,5 @@ void give_unicorn_rewards() { // Display rewards in text reader text_reader_lines(rewards, "Unicorn Victory", true); + attempt_pet_offer_from_adventure(); } diff --git a/src/constants.nvgt b/src/constants.nvgt index 31f2d9a..c7b8597 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -187,6 +187,9 @@ const int EXPANSION_ROAMER_MAX_PER_AREA = 3; const int AUDIO_TILE_SCALE = 10; const float AUDIO_PAN_STEP = 2.0; const float AUDIO_VOLUME_STEP = 3.0; +const float MASTER_VOLUME_MAX_DB = 0.0; +const float MASTER_VOLUME_MIN_DB = -60.0; +const float MASTER_VOLUME_STEP_DB = 3.0; const int SNARE_SOUND_RANGE = 2; const float SNARE_SOUND_VOLUME_STEP = 4.0; // More audible for locating snares const float SNARE_SOUND_PAN_STEP = 4.0; // Stronger pan for direction @@ -245,6 +248,24 @@ const float PLAYER_ITEM_BREAK_CHANCE_MIN = 1.0; const float PLAYER_ITEM_BREAK_CHANCE_MAX = 100.0; const float PLAYER_ITEM_BREAK_CHANCE_INCREMENT = 0.1; const int PLAYER_ITEM_BREAKS_PER_DAY = 2; +const int PLAYER_ITEM_BREAK_ROLL_MAX = 200; + +// Pet system +const int PET_START_LOYALTY = 5; +const int PET_LOYALTY_HELP_LOSS = 1; +const int PET_LOYALTY_HUNGER_LOSS = 2; +const int PET_LOYALTY_ACTION_COST = 1; +const int PET_LOYALTY_EAT_BONUS = 2; +const int PET_LOYALTY_MAX = 10; +const int PET_ATTACK_COOLDOWN = 1600; // Same as axe +const int PET_RETRIEVE_COOLDOWN = 1000; +const int PET_RANDOM_FIND_CHANCE = 20; // Percent per hour when loyalty is high +const int PET_ADVENTURE_CHANCE = 5; // Percent chance after adventure victory +const int PET_TREE_HAWK_CHANCE = 10; // Percent chance after reaching top of a tree +const int PET_LOYALTY_BONUS_THRESHOLD = 5; +const int PET_MOVE_SPEED = 320; // Slightly faster than base walk speed +const int PET_TRAVEL_MIN_MS = 100; +const int PET_RANGE = BOW_RANGE + 2; // Goose settings const int GOOSE_HEALTH = 1; diff --git a/src/crafting/craft_barricade.nvgt b/src/crafting/craft_barricade.nvgt index 018987a..9021881 100644 --- a/src/crafting/craft_barricade.nvgt +++ b/src/crafting/craft_barricade.nvgt @@ -5,8 +5,6 @@ void run_barricade_menu() { return; } - speak_with_history("Barricade.", true); - int selection = 0; string[] options; int[] action_types; // 0 = sticks, 1 = vines, 2 = log, 3 = stones @@ -32,6 +30,7 @@ void run_barricade_menu() { speak_with_history("No materials to reinforce the barricade.", true); return; } + speak_with_history("Barricade. " + options[selection], true); while(true) { wait(5); diff --git a/src/crafting/craft_buildings.nvgt b/src/crafting/craft_buildings.nvgt index 5982a85..2e09ea5 100644 --- a/src/crafting/craft_buildings.nvgt +++ b/src/crafting/craft_buildings.nvgt @@ -26,8 +26,6 @@ bool has_building_options() { } void run_buildings_menu() { - speak_with_history("Buildings.", true); - int selection = 0; string[] options; int[] building_types; @@ -95,6 +93,7 @@ void run_buildings_menu() { speak_with_history("No buildings available.", true); return; } + speak_with_history("Buildings. " + options[selection], true); while(true) { wait(5); diff --git a/src/crafting/craft_clothing.nvgt b/src/crafting/craft_clothing.nvgt index a2b5961..c9fffa8 100644 --- a/src/crafting/craft_clothing.nvgt +++ b/src/crafting/craft_clothing.nvgt @@ -43,8 +43,6 @@ void consume_pouches(int amount) { } void run_clothing_menu() { - speak_with_history("Clothing.", true); - int selection = 0; string[] options = { "Skin Hat (1 Skin, 1 Vine)", @@ -55,6 +53,7 @@ void run_clothing_menu() { "Skin Pouch (2 Skins, 1 Vine)", "Backpack (11 Skins, 5 Vines, 4 Skin Pouches)" }; + speak_with_history("Clothing. " + options[selection], true); while(true) { wait(5); diff --git a/src/crafting/craft_materials.nvgt b/src/crafting/craft_materials.nvgt index f1a1ee1..f631e02 100644 --- a/src/crafting/craft_materials.nvgt +++ b/src/crafting/craft_materials.nvgt @@ -1,7 +1,5 @@ // Crafting materials void run_materials_menu() { - speak_with_history("Materials.", true); - int selection = 0; string[] options = { "Butcher Game [Requires Game, Knife, Fire nearby]", @@ -10,6 +8,7 @@ void run_materials_menu() { "Bowstring (3 Sinew) [Requires Fire nearby]", "Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]" }; + speak_with_history("Materials. " + options[selection], true); while(true) { wait(5); diff --git a/src/crafting/craft_runes.nvgt b/src/crafting/craft_runes.nvgt index c481db1..b88e593 100644 --- a/src/crafting/craft_runes.nvgt +++ b/src/crafting/craft_runes.nvgt @@ -75,8 +75,6 @@ void run_runes_menu() { return; } - speak_with_history("Runes.", true); - // Build list of unlocked runes string[] rune_options; int[] rune_types; @@ -95,6 +93,7 @@ void run_runes_menu() { } int selection = 0; + speak_with_history("Runes. " + rune_options[selection], true); while(true) { wait(5); @@ -130,8 +129,6 @@ void run_runes_menu() { } void run_rune_equipment_menu(int rune_type) { - speak_with_history("Select equipment to engrave with " + get_rune_name(rune_type) + ".", true); - // Build list of equipment that can be runed string[] equipment_options; int[] equipment_types; @@ -153,6 +150,7 @@ void run_rune_equipment_menu(int rune_type) { } int selection = 0; + speak_with_history("Select equipment to engrave with " + get_rune_name(rune_type) + ". " + equipment_options[selection], true); while(true) { wait(5); diff --git a/src/crafting/craft_tools.nvgt b/src/crafting/craft_tools.nvgt index 6a7a83c..7b77873 100644 --- a/src/crafting/craft_tools.nvgt +++ b/src/crafting/craft_tools.nvgt @@ -1,7 +1,5 @@ // Crafting tools void run_tools_menu() { - speak_with_history("Tools.", true); - int selection = 0; string[] options = { "Stone Knife (2 Stones)", @@ -14,6 +12,7 @@ void run_tools_menu() { "Reed Basket (3 Reeds)", "Clay Pot (3 Clay)" }; + speak_with_history("Tools. " + options[selection], true); while(true) { wait(5); diff --git a/src/crafting/craft_weapons.nvgt b/src/crafting/craft_weapons.nvgt index 3f0d1c1..de36f6c 100644 --- a/src/crafting/craft_weapons.nvgt +++ b/src/crafting/craft_weapons.nvgt @@ -1,13 +1,12 @@ // Crafting weapons void run_weapons_menu() { - speak_with_history("Weapons.", true); - int selection = 0; string[] options = { "Spear (1 Stick, 1 Vine, 1 Stone) [Requires Knife]", "Sling (1 Skin, 2 Vines)", "Bow (1 Stick, 1 Bowstring)" }; + speak_with_history("Weapons. " + options[selection], true); while(true) { wait(5); diff --git a/src/crafting/crafting_core.nvgt b/src/crafting/crafting_core.nvgt index e98716e..d0ffe4d 100644 --- a/src/crafting/crafting_core.nvgt +++ b/src/crafting/crafting_core.nvgt @@ -8,8 +8,6 @@ void check_crafting_menu(int x, int base_end_tile) { } void run_crafting_menu() { - speak_with_history("Crafting menu.", true); - int selection = 0; string[] categories; int[] category_types; @@ -35,6 +33,7 @@ void run_crafting_menu() { categories.insert_last("Runes"); category_types.insert_last(6); } + speak_with_history("Crafting menu. " + categories[selection], true); while(true) { wait(5); diff --git a/src/creature_audio.nvgt b/src/creature_audio.nvgt index 68f075c..d650a80 100644 --- a/src/creature_audio.nvgt +++ b/src/creature_audio.nvgt @@ -96,6 +96,13 @@ string get_creature_death_sound_from_alert(string alert_sound) filename = filename.substr(0, dot_index); } + if (filename == "bandit3" || filename == "bandit4") { + string female_death = "sounds/enemies/bandit_female_dies.ogg"; + if (file_exists(female_death)) { + return female_death; + } + } + // Strip trailing digits (bandit1 -> bandit, goblin2 -> goblin) while (filename.length() > 0) { string last_char = filename.substr(filename.length() - 1, 1); diff --git a/src/environment.nvgt b/src/environment.nvgt index 5c36005..731df03 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -936,6 +936,7 @@ void update_climbing() { int ground_elevation = get_mountain_elevation_at(x); int height_above_ground = y - ground_elevation; speak_with_history("Reached the top at " + height_above_ground + " feet.", true); + attempt_pet_offer_from_tree(); } } // Climbing down diff --git a/src/inventory_menus.nvgt b/src/inventory_menus.nvgt index 62d53e1..2422a4f 100644 --- a/src/inventory_menus.nvgt +++ b/src/inventory_menus.nvgt @@ -28,7 +28,7 @@ // Main key check functions (called from main game loop) void check_inventory_keys(int x) { if (key_pressed(KEY_P)) { - show_character_info(); + run_character_info_menu(); return; } if (key_pressed(KEY_I)) { diff --git a/src/learn_sounds.nvgt b/src/learn_sounds.nvgt index 9206579..9de7829 100644 --- a/src/learn_sounds.nvgt +++ b/src/learn_sounds.nvgt @@ -221,6 +221,7 @@ void run_learn_sounds_menu() { while (true) { wait(5); + handle_global_volume_keys(); if (key_pressed(KEY_ESCAPE)) { speak_with_history("Closed.", true); diff --git a/src/menus/base_info.nvgt b/src/menus/base_info.nvgt index 823fcd6..f9077eb 100644 --- a/src/menus/base_info.nvgt +++ b/src/menus/base_info.nvgt @@ -7,8 +7,6 @@ void run_base_info_menu() { return; } - speak_with_history("Base info.", true); - int selection = 0; string[] options; options.insert_last("Barricade health " + barricade_health + " of " + BARRICADE_MAX_HEALTH); @@ -53,6 +51,11 @@ void run_base_info_menu() { int[] filtered_indices; string[] filtered_options; apply_menu_filter(filter_text, options, filtered_indices, filtered_options); + if (filtered_options.length() == 0) { + speak_with_history("Base info. No options.", true); + } else { + speak_with_history("Base info. " + filtered_options[selection], true); + } while(true) { wait(5); diff --git a/src/menus/character_info.nvgt b/src/menus/character_info.nvgt index f7da924..6bef64c 100644 --- a/src/menus/character_info.nvgt +++ b/src/menus/character_info.nvgt @@ -1,7 +1,7 @@ // Character info display // Functions for displaying character stats and equipment -void show_character_info() { +void run_character_info_menu() { string[] equipped_clothing; string[] missing_slots; @@ -19,22 +19,85 @@ void show_character_info() { if (equipped_feet == EQUIP_MOCCASINS) equipped_clothing.insert_last("Moccasins"); else missing_slots.insert_last("feet"); - string info = "Character info. "; + string[] options; if (player_name != "") { string sex_label = (player_sex == SEX_FEMALE) ? "Female" : "Male"; - info += "Name " + player_name + ". Sex " + sex_label + ". "; - } - info += "Health " + player_health + " of " + max_health + ". "; - info += "Weapon " + get_equipped_weapon_name() + ". "; - if (equipped_clothing.length() > 0) { - info += "Clothing equipped: " + join_string_list(equipped_clothing) + ". "; + options.insert_last("Name " + player_name + ". Sex " + sex_label + "."); } else { - info += "No clothing equipped. "; + options.insert_last("Name unknown."); + } + options.insert_last("Health " + player_health + " of " + max_health + "."); + options.insert_last("Weapon " + get_equipped_weapon_name() + "."); + if (equipped_clothing.length() > 0) { + options.insert_last("Clothing equipped: " + join_string_list(equipped_clothing) + "."); + } else { + options.insert_last("No clothing equipped."); } if (missing_slots.length() > 0) { - info += "Missing " + join_string_list(missing_slots) + ". "; + options.insert_last("Missing " + join_string_list(missing_slots) + "."); + } + options.insert_last("Favor " + format_favor(favor) + "."); + options.insert_last("Speed " + get_speed_status() + "."); + if (petActive) { + options.insert_last("Pet " + petType + ". Gender " + petGender + ". Loyalty " + petLoyalty + "."); + } else { + options.insert_last("Pet none."); + } + + int selection = 0; + string filter_text = ""; + int[] filtered_indices; + string[] filtered_options; + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); + if (filtered_options.length() == 0) { + speak_with_history("Character info. No options.", true); + } else { + speak_with_history("Character info. " + filtered_options[selection], true); + } + + while(true) { + wait(5); + if (menu_background_tick()) { + return; + } + if (key_pressed(KEY_ESCAPE)) { + speak_with_history("Closed.", true); + break; + } + + bool filter_changed = update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); + if (filter_changed) { + if (filtered_options.length() == 0) { + if (filter_text.length() > 0) { + speak_with_history("No matches for " + filter_text + ".", true); + } else { + speak_with_history("No options.", true); + } + } else { + speak_with_history(filtered_options[selection], true); + } + } + + if (key_pressed(KEY_DOWN)) { + if (filtered_options.length() > 0) { + play_menu_move_sound(); + selection++; + if (selection >= int(filtered_options.length())) selection = 0; + speak_with_history(filtered_options[selection], true); + } + } + + if (key_pressed(KEY_UP)) { + if (filtered_options.length() > 0) { + play_menu_move_sound(); + selection--; + if (selection < 0) selection = int(filtered_options.length()) - 1; + speak_with_history(filtered_options[selection], true); + } + } } - info += "Favor " + format_favor(favor) + ". "; - info += "Speed " + get_speed_status() + "."; - speak_with_history(info, true); +} + +void show_character_info() { + run_character_info_menu(); } diff --git a/src/menus/menu_utils.nvgt b/src/menus/menu_utils.nvgt index 15660c3..9c5189f 100644 --- a/src/menus/menu_utils.nvgt +++ b/src/menus/menu_utils.nvgt @@ -5,6 +5,7 @@ bool menu_background_tick() { if (return_to_main_menu_requested) { return true; } + handle_global_volume_keys(); if (!menuBackgroundUpdatesEnabled) { return false; } @@ -22,6 +23,7 @@ bool menu_background_tick() { update_flying_creatures(); update_weapon_range_audio_all(); update_world_drops(); + update_pets(); update_blessings(); update_notifications(); diff --git a/src/pet_system.nvgt b/src/pet_system.nvgt new file mode 100644 index 0000000..6bb364e --- /dev/null +++ b/src/pet_system.nvgt @@ -0,0 +1,647 @@ +// Pet system +// Handles pet acquisition, loyalty, feeding, retrieval, and combat support + +bool petActive = false; +string petSoundPath = ""; +string petType = ""; +string petGender = ""; +int petLoyalty = 0; +bool petOut = false; + +timer petAttackTimer; +timer petRetrieveTimer; + +string[] petEventMessages; +string[] petEventSounds; +int[] petEventPositions; +int petEventSoundHandle = -1; + +const int PET_TRAVEL_NONE = 0; +const int PET_TRAVEL_ATTACK = 1; +const int PET_TRAVEL_RETRIEVE = 2; + +bool petTravelActive = false; +int petTravelAction = PET_TRAVEL_NONE; +int petTravelStartPos = 0; +int petTravelTargetPos = -1; +int petTravelTargetKind = -1; +string petTravelTargetLabel = ""; +int petTravelDurationMs = 0; +int petTravelSoundHandle = -1; +timer petTravelTimer; + +string[] petSoundPaths; +bool petSoundsInitialized = false; + +string normalize_pet_path(const string&in path) { + return path.replace("\\", "/", true); +} + +string collapse_pet_spaces(const string&in text) { + string result = text; + while (result.find_first(" ") > -1) { + result = result.replace(" ", " ", true); + } + return result; +} + +void gather_pet_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_pet_sound_files(basePath + "/" + folders[i], outFiles); + } + } +} + +bool sort_pet_sound_paths(const string &in a, const string &in b) { + return a.lower() < b.lower(); +} + +void init_pet_sounds() { + if (petSoundsInitialized) return; + petSoundsInitialized = true; + petSoundPaths.resize(0); + if (!directory_exists("sounds/pets")) return; + gather_pet_sound_files("sounds/pets", petSoundPaths); + if (petSoundPaths.length() > 1) { + petSoundPaths.sort(sort_pet_sound_paths); + } +} + +string get_pet_name_from_sound_path(const string&in soundPath) { + string normalizedPath = normalize_pet_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_pet_spaces(name); + name = name.lower(); + name.trim_whitespace_this(); + if (name.length() == 0) return "Pet"; + string first = name.substr(0, 1).upper(); + if (name.length() == 1) return first; + return first + name.substr(1); +} + +string get_pet_sound_for_name(const string&in petName) { + init_pet_sounds(); + string target = petName.lower(); + for (uint i = 0; i < petSoundPaths.length(); i++) { + string normalizedPath = normalize_pet_path(petSoundPaths[i]); + int slashPos = normalizedPath.find_last_of("/"); + string fileName = (slashPos >= 0) ? normalizedPath.substr(slashPos + 1) : normalizedPath; + string lowerName = fileName.lower(); + if (lowerName.length() > 4 && lowerName.substr(lowerName.length() - 4) == ".ogg") { + lowerName = lowerName.substr(0, lowerName.length() - 4); + } + if (lowerName == target) { + return petSoundPaths[i]; + } + } + return ""; +} + +int get_pet_food_personal_total() { + return get_personal_count(ITEM_MEAT) + + get_personal_count(ITEM_SMOKED_FISH) + + get_personal_count(ITEM_FISH) + + get_personal_count(ITEM_BASKET_FOOD); +} + +int get_pet_food_storage_total() { + return get_storage_count(ITEM_MEAT) + + get_storage_count(ITEM_SMOKED_FISH) + + get_storage_count(ITEM_FISH) + + get_storage_count(ITEM_BASKET_FOOD); +} + +bool has_pet_food_available() { + return get_pet_food_personal_total() + get_pet_food_storage_total() > 0; +} + +bool consume_pet_food() { + if (get_personal_count(ITEM_MEAT) > 0) { + add_personal_count(ITEM_MEAT, -1); + return true; + } + if (get_personal_count(ITEM_SMOKED_FISH) > 0) { + add_personal_count(ITEM_SMOKED_FISH, -1); + return true; + } + if (get_personal_count(ITEM_FISH) > 0) { + pop_personal_fish_weight(); + add_personal_count(ITEM_FISH, -1); + return true; + } + if (get_personal_count(ITEM_BASKET_FOOD) > 0) { + add_personal_count(ITEM_BASKET_FOOD, -1); + return true; + } + if (get_storage_count(ITEM_MEAT) > 0) { + add_storage_count(ITEM_MEAT, -1); + return true; + } + if (get_storage_count(ITEM_SMOKED_FISH) > 0) { + add_storage_count(ITEM_SMOKED_FISH, -1); + return true; + } + if (get_storage_count(ITEM_FISH) > 0) { + pop_storage_fish_weight(); + add_storage_count(ITEM_FISH, -1); + return true; + } + if (get_storage_count(ITEM_BASKET_FOOD) > 0) { + add_storage_count(ITEM_BASKET_FOOD, -1); + return true; + } + return false; +} + +void queue_pet_event(const string&in message, int soundPos = -1) { + if (!petActive) return; + if (message.length() == 0) return; + petEventMessages.insert_last(message); + petEventSounds.insert_last(petSoundPath); + petEventPositions.insert_last(soundPos); +} + +void update_pet_events() { + if (petEventMessages.length() == 0) { + petEventSoundHandle = -1; + return; + } + + if (petEventSoundHandle != -1) { + if (p.sound_is_active(petEventSoundHandle)) { + return; + } + string message = petEventMessages[0]; + petEventMessages.remove_at(0); + petEventSounds.remove_at(0); + petEventPositions.remove_at(0); + petEventSoundHandle = -1; + speak_with_history(message, true); + return; + } + + string soundPath = petEventSounds[0]; + int soundPos = petEventPositions[0]; + if (soundPath != "" && file_exists(soundPath)) { + if (soundPos >= 0) { + petEventSoundHandle = play_1d_with_volume_step(soundPath, x, soundPos, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + } else { + petEventSoundHandle = p.play_stationary(soundPath, false); + } + if (petEventSoundHandle != -1) { + return; + } + } + + string message = petEventMessages[0]; + petEventMessages.remove_at(0); + petEventSounds.remove_at(0); + petEventPositions.remove_at(0); + speak_with_history(message, true); +} + +void reset_pet_state() { + petActive = false; + petSoundPath = ""; + petType = ""; + petGender = ""; + petLoyalty = 0; + petOut = false; + petEventMessages.resize(0); + petEventSounds.resize(0); + petEventPositions.resize(0); + petEventSoundHandle = -1; + petTravelActive = false; + petTravelAction = PET_TRAVEL_NONE; + petTravelStartPos = 0; + petTravelTargetPos = -1; + petTravelTargetKind = -1; + petTravelTargetLabel = ""; + petTravelDurationMs = 0; + safe_destroy_sound(petTravelSoundHandle); + petTravelSoundHandle = -1; + petAttackTimer.restart(); + petRetrieveTimer.restart(); + petTravelTimer.restart(); +} + +void pet_leave() { + string oldPet = petType; + reset_pet_state(); + if (oldPet != "") { + speak_with_history(oldPet + " leaves.", true); + } +} + +void clamp_pet_loyalty() { + if (petLoyalty < 0) petLoyalty = 0; + if (petLoyalty > PET_LOYALTY_MAX) petLoyalty = PET_LOYALTY_MAX; +} + +void adjust_pet_loyalty(int delta) { + if (!petActive) return; + petLoyalty += delta; + clamp_pet_loyalty(); + if (petLoyalty <= 0) { + pet_leave(); + } +} + +void check_pet_call_key() { + if (!key_pressed(KEY_SPACE)) return; + if (!petActive) { + speak_with_history("No pet.", true); + return; + } + if (petOut) { + petOut = false; + stop_pet_travel(); + speak_with_history("Pet called back.", true); + return; + } + petOut = true; + if (file_exists("sounds/pets/call_pet.ogg")) { + p.play_stationary("sounds/pets/call_pet.ogg", false); + } +} + +void adopt_pet(const string&in soundPath) { + petActive = true; + petSoundPath = soundPath; + petType = get_pet_name_from_sound_path(soundPath); + petGender = (random(0, 1) == 0) ? "Male" : "Female"; + petLoyalty = PET_START_LOYALTY; + petOut = false; + petAttackTimer.restart(); + petRetrieveTimer.restart(); + petTravelTimer.restart(); + speak_with_history("A " + petType + " joins you.", true); +} + +void stop_pet_travel() { + petTravelActive = false; + petTravelAction = PET_TRAVEL_NONE; + petTravelStartPos = 0; + petTravelTargetPos = -1; + petTravelTargetKind = -1; + petTravelTargetLabel = ""; + petTravelDurationMs = 0; + safe_destroy_sound(petTravelSoundHandle); + petTravelSoundHandle = -1; +} + +int get_pet_travel_duration_ms(int targetPos) { + int distance = abs(targetPos - x); + int duration = distance * PET_MOVE_SPEED; + if (duration < PET_TRAVEL_MIN_MS) duration = PET_TRAVEL_MIN_MS; + return duration; +} + +void start_pet_travel_attack(int targetPos, const string&in targetLabel, int targetKind) { + stop_pet_travel(); + petTravelActive = true; + petTravelAction = PET_TRAVEL_ATTACK; + petTravelStartPos = x; + petTravelTargetPos = targetPos; + petTravelTargetLabel = targetLabel; + petTravelTargetKind = targetKind; + petTravelDurationMs = get_pet_travel_duration_ms(targetPos); + petTravelTimer.restart(); + + if (petSoundPath != "" && file_exists(petSoundPath)) { + petTravelSoundHandle = play_1d_with_volume_step( + petSoundPath, + x, + petTravelStartPos, + true, + PLAYER_WEAPON_SOUND_VOLUME_STEP + ); + } +} + +void start_pet_travel_retrieve(int targetPos) { + stop_pet_travel(); + petTravelActive = true; + petTravelAction = PET_TRAVEL_RETRIEVE; + petTravelStartPos = x; + petTravelTargetPos = targetPos; + petTravelDurationMs = get_pet_travel_duration_ms(targetPos); + petTravelTimer.restart(); +} + +void update_pet_travel() { + if (!petTravelActive) return; + if (petTravelDurationMs < 1) petTravelDurationMs = 1; + + int elapsed = petTravelTimer.elapsed; + float progress = float(elapsed) / float(petTravelDurationMs); + if (progress > 1.0f) progress = 1.0f; + + int travel = int(float(petTravelTargetPos - petTravelStartPos) * progress); + int currentPos = petTravelStartPos + travel; + if (petTravelSoundHandle != -1) { + p.update_sound_1d(petTravelSoundHandle, currentPos); + } + + if (elapsed < petTravelDurationMs) return; + + safe_destroy_sound(petTravelSoundHandle); + petTravelSoundHandle = -1; + + if (petTravelAction == PET_TRAVEL_ATTACK) { + int damage = BOW_DAMAGE_MAX; + bool hit = false; + if (petTravelTargetKind == 0) { + hit = damage_bandit_at(petTravelTargetPos, damage); + } else if (petTravelTargetKind == 1) { + hit = damage_undead_at(petTravelTargetPos, damage); + } else if (petTravelTargetKind == 2) { + hit = damage_boar_at(petTravelTargetPos, damage); + } + if (hit) { + queue_pet_event("Your " + petType + " attacks the " + petTravelTargetLabel + ".", petTravelTargetPos); + } + adjust_pet_loyalty(-PET_LOYALTY_ACTION_COST); + } else if (petTravelAction == PET_TRAVEL_RETRIEVE) { + WorldDrop@ drop = get_drop_at(petTravelTargetPos); + if (drop !is null) { + string message = ""; + if (try_pet_pickup_world_drop(drop, message)) { + remove_drop_at(drop.position); + queue_pet_event(message); + petOut = false; + } + } + adjust_pet_loyalty(-PET_LOYALTY_ACTION_COST); + } + + stop_pet_travel(); +} + +bool run_pet_offer_menu(const string&in soundPath, const string&in reasonText) { + if (petActive) return false; + if (soundPath == "" || !file_exists(soundPath)) return false; + if (!has_pet_food_available()) return false; + + string petName = get_pet_name_from_sound_path(soundPath); + string prompt = "A friendly looking " + petName + " begs for food. Accept?"; + if (reasonText != "") { + prompt += " " + reasonText; + } + + string[] options; + options.insert_last("Yes"); + options.insert_last("No"); + int selection = 0; + + speak_with_history(prompt + " " + options[selection], true); + + while (true) { + wait(5); + if (menu_background_tick()) { + return false; + } + if (key_pressed(KEY_ESCAPE)) { + speak_with_history("Declined.", true); + return false; + } + if (key_pressed(KEY_DOWN)) { + play_menu_move_sound(); + selection++; + if (selection >= int(options.length())) selection = 0; + speak_with_history(options[selection], true); + } + if (key_pressed(KEY_UP)) { + play_menu_move_sound(); + selection--; + if (selection < 0) selection = int(options.length()) - 1; + speak_with_history(options[selection], true); + } + if (key_pressed(KEY_RETURN)) { + play_menu_select_sound(); + if (selection == 0) { + adopt_pet(soundPath); + return true; + } + speak_with_history("Declined.", true); + return false; + } + } + return false; +} + +void attempt_pet_offer_random(const string&in reasonText) { + if (petActive) return; + if (!has_pet_food_available()) return; + init_pet_sounds(); + if (petSoundPaths.length() == 0) return; + int pick = random(0, petSoundPaths.length() - 1); + run_pet_offer_menu(petSoundPaths[pick], reasonText); +} + +void attempt_pet_offer_from_quest(int score) { + if (score < QUEST_LOG_SCORE) return; + attempt_pet_offer_random(""); +} + +void attempt_pet_offer_from_adventure() { + if (petActive) return; + if (random(1, 100) > PET_ADVENTURE_CHANCE) return; + attempt_pet_offer_random(""); +} + +void attempt_pet_offer_from_tree() { + if (petActive) return; + if (random(1, 100) > PET_TREE_HAWK_CHANCE) return; + string hawkSound = get_pet_sound_for_name("hawk"); + if (hawkSound == "") return; + run_pet_offer_menu(hawkSound, ""); +} + +bool try_pet_pickup_small_game(const string&in gameType, string &out message) { + if (get_personal_count(ITEM_SMALL_GAME) >= get_personal_stack_limit()) { + return false; + } + add_personal_count(ITEM_SMALL_GAME, 1); + personal_small_game_types.insert_last(gameType); + message = "Your " + petType + " retrieved " + gameType + "."; + return true; +} + +bool try_pet_pickup_world_drop(WorldDrop@ drop, string &out message) { + if (drop is null) return false; + if (get_flying_creature_config_by_drop_type(drop.type) !is null) { + return try_pet_pickup_small_game(drop.type, message); + } + if (drop.type == "arrow") { + int maxArrows = get_arrow_limit(); + if (maxArrows <= 0) { + return false; + } + if (get_personal_count(ITEM_ARROWS) >= maxArrows) { + return false; + } + add_personal_count(ITEM_ARROWS, 1); + message = "Your " + petType + " retrieved an arrow."; + return true; + } + if (drop.type == "boar carcass") { + if (get_personal_count(ITEM_BOAR_CARCASSES) >= get_personal_stack_limit()) { + return false; + } + add_personal_count(ITEM_BOAR_CARCASSES, 1); + message = "Your " + petType + " retrieved a boar carcass."; + return true; + } + return false; +} + +WorldDrop@ find_pet_drop_target() { + int bestDistance = PET_RANGE + 1; + WorldDrop@ best = null; + for (uint i = 0; i < world_drops.length(); i++) { + int distance = abs(world_drops[i].position - x); + if (distance > PET_RANGE) continue; + if (distance < bestDistance) { + bestDistance = distance; + @best = world_drops[i]; + } + } + return best; +} + +void update_pet_retrieval() { + if (!petActive) return; + if (!petOut) return; + if (petLoyalty <= 0) return; + if (petRetrieveTimer.elapsed < PET_RETRIEVE_COOLDOWN) return; + if (petEventMessages.length() > 2) return; + if (petTravelActive) return; + + WorldDrop@ drop = find_pet_drop_target(); + if (drop is null) return; + + petRetrieveTimer.restart(); + start_pet_travel_retrieve(drop.position); +} + +bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &out targetKind) { + int bestDistance = PET_RANGE + 1; + targetPos = -1; + targetLabel = ""; + targetKind = -1; + + for (uint i = 0; i < bandits.length(); i++) { + int distance = abs(bandits[i].position - x); + if (distance > PET_RANGE) continue; + if (distance < bestDistance) { + bestDistance = distance; + targetPos = bandits[i].position; + targetLabel = "bandit"; + targetKind = 0; + } + } + + for (uint i = 0; i < undeads.length(); i++) { + if (undeads[i].undead_type == "undead_resident") continue; + int distance = abs(undeads[i].position - x); + if (distance > PET_RANGE) continue; + if (distance < bestDistance) { + bestDistance = distance; + targetPos = undeads[i].position; + targetLabel = "undead"; + targetKind = 1; + } + } + + for (uint i = 0; i < ground_games.length(); i++) { + int distance = abs(ground_games[i].position - x); + if (distance > PET_RANGE) continue; + if (distance < bestDistance) { + bestDistance = distance; + targetPos = ground_games[i].position; + targetLabel = "boar"; + targetKind = 2; + } + } + + return targetPos != -1; +} + +void update_pet_attack() { + if (!petActive) return; + if (!petOut) return; + if (petLoyalty <= 0) return; + if (petAttackTimer.elapsed < PET_ATTACK_COOLDOWN) return; + if (petTravelActive) return; + + int targetPos = -1; + string targetLabel = ""; + int targetKind = -1; + if (!find_pet_attack_target(targetPos, targetLabel, targetKind)) return; + + petAttackTimer.restart(); + start_pet_travel_attack(targetPos, targetLabel, targetKind); +} + +void attempt_pet_random_find() { + if (!petActive) return; + if (!petOut) return; + if (petLoyalty < PET_LOYALTY_BONUS_THRESHOLD) return; + if (random(1, 100) > PET_RANDOM_FIND_CHANCE) return; + + int[] possibleItems = {ITEM_STICKS, ITEM_VINES, ITEM_STONES, ITEM_CLAY}; + int itemType = possibleItems[random(0, possibleItems.length() - 1)]; + int added = add_to_stack(get_personal_count(itemType), 1); + if (added <= 0) return; + + add_personal_count(itemType, added); + string itemName = (added == 1) ? item_registry[itemType].singular : item_registry[itemType].name; + queue_pet_event("Your " + petType + " retrieved " + added + " " + itemName + ".", x); + adjust_pet_loyalty(-PET_LOYALTY_ACTION_COST); + petOut = false; +} + +void handle_pet_hourly_update(int hour) { + if (!petActive) return; + + if (get_pet_food_personal_total() == 0) { + adjust_pet_loyalty(-PET_LOYALTY_HUNGER_LOSS); + } + + if (!petActive) return; + + if (hour % 8 == 0) { + if (consume_pet_food()) { + petLoyalty += PET_LOYALTY_EAT_BONUS; + clamp_pet_loyalty(); + } + } + + attempt_pet_random_find(); +} + +void update_pets() { + update_pet_travel(); + if (petActive && petOut) { + update_pet_retrieval(); + update_pet_attack(); + } + update_pet_events(); +} diff --git a/src/quest_system.nvgt b/src/quest_system.nvgt index 7b9dfd4..0620af0 100644 --- a/src/quest_system.nvgt +++ b/src/quest_system.nvgt @@ -153,6 +153,7 @@ void apply_quest_reward(int score) { message += "\nScore: " + score; text_reader(message, "Quest Rewards", true); + attempt_pet_offer_from_quest(score); } void run_quest(int quest_type) { diff --git a/src/quests/bat_invasion_game.nvgt b/src/quests/bat_invasion_game.nvgt index ed14d32..887bae6 100644 --- a/src/quests/bat_invasion_game.nvgt +++ b/src/quests/bat_invasion_game.nvgt @@ -51,6 +51,7 @@ int run_bat_invasion() { while (flight_timer.elapsed < flight_time) { wait(update_interval); + handle_global_volume_keys(); // Calculate current position based on elapsed time float progress = float(flight_timer.elapsed) / float(flight_time); diff --git a/src/quests/catch_the_boomerang_game.nvgt b/src/quests/catch_the_boomerang_game.nvgt index 5bf27d5..5e1da2c 100644 --- a/src/quests/catch_the_boomerang_game.nvgt +++ b/src/quests/catch_the_boomerang_game.nvgt @@ -32,6 +32,7 @@ int run_catch_the_boomerang() { while (true) { wait(5); + handle_global_volume_keys(); if (key_pressed(KEY_SPACE)) { break; } @@ -46,6 +47,7 @@ int run_catch_the_boomerang() { while (!resolved) { wait(stepDelay); + handle_global_volume_keys(); p.play_2d(boomerangSound, 0.0, 0.0, 0.0, boomerangY, false); diff --git a/src/quests/enchanted_melody_game.nvgt b/src/quests/enchanted_melody_game.nvgt index 5cc57ce..78c738d 100644 --- a/src/quests/enchanted_melody_game.nvgt +++ b/src/quests/enchanted_melody_game.nvgt @@ -41,6 +41,7 @@ void run_practice_mode() { while (true) { wait(5); + handle_global_volume_keys(); // Check for practice note input int note = get_note_from_key(); @@ -79,12 +80,14 @@ int run_enchanted_melody() { while (true) { for (uint i = 0; i < pattern.length(); i++) { play_note(pattern[i]); + handle_global_volume_keys(); wait(600); } uint index = 0; while (index < pattern.length()) { wait(5); + handle_global_volume_keys(); int note = get_note_from_key(); if (note == -1) continue; play_note(note); diff --git a/src/quests/escape_from_hel_game.nvgt b/src/quests/escape_from_hel_game.nvgt index b832c2a..5711a3c 100644 --- a/src/quests/escape_from_hel_game.nvgt +++ b/src/quests/escape_from_hel_game.nvgt @@ -44,6 +44,8 @@ int run_escape_from_hel() { int step_time = base_step_time - (total_steps * 8); if (step_time < 50) step_time = 50; // Minimum to prevent audio/timing issues + handle_global_volume_keys(); + // Check if jump finished if (jumping && jump_timer.elapsed >= JUMP_DURATION) { jumping = false; @@ -61,6 +63,7 @@ int run_escape_from_hel() { // Wait for step duration, checking for jump input while (step_timer.elapsed < step_time) { wait(5); + handle_global_volume_keys(); // Allow jump anytime when not already jumping if (!jumping && key_pressed(KEY_SPACE)) { diff --git a/src/quests/skeletal_bard_game.nvgt b/src/quests/skeletal_bard_game.nvgt index 21bb3c7..13d28f4 100644 --- a/src/quests/skeletal_bard_game.nvgt +++ b/src/quests/skeletal_bard_game.nvgt @@ -87,6 +87,7 @@ int run_skeletal_bard() { speak_with_history("You entered " + guess + " notes and the actual number was " + length + ". " + points + " points. Press Enter to continue.", true); while (true) { wait(5); + handle_global_volume_keys(); if (return_to_main_menu_requested) { return 0; } diff --git a/src/save_system.nvgt b/src/save_system.nvgt index f14d5c1..4c03765 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -356,7 +356,7 @@ string pick_random_name_for_sex(int sex, const string[]@ usedNames) { bool select_player_sex(int &out sex) { string[] options = {"Male", "Female"}; int selection = 0; - string prompt = "Choose your character's sex."; + string prompt = "Choose your sex."; speak_with_history(prompt + " " + options[selection], true); while (true) { @@ -365,13 +365,13 @@ bool select_player_sex(int &out sex) { play_menu_move_sound(); selection++; if (selection >= options.length()) selection = 0; - speak_with_history(prompt + " " + options[selection], true); + 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(prompt + " " + options[selection], true); + speak_with_history(options[selection], true); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); @@ -586,6 +586,7 @@ void reset_game_state() { blessing_speed_active = false; blessing_resident_active = false; reset_fylgja_state(); + reset_pet_state(); // Reset inventory using the registry system reset_inventory(); @@ -800,6 +801,11 @@ bool save_game_state() { saveData.set("adventure_completion_counts", serialize_inventory_array(adventureCompletionCounts)); saveData.set("incense_hours_remaining", incense_hours_remaining); saveData.set("incense_burning", incense_burning); + saveData.set("pet_active", petActive); + saveData.set("pet_sound_path", petSoundPath); + saveData.set("pet_type", petType); + saveData.set("pet_gender", petGender); + saveData.set("pet_loyalty", petLoyalty); // Save inventory arrays using new compact format saveData.set("personal_inventory", serialize_inventory_array(personal_inventory)); @@ -1106,6 +1112,40 @@ bool load_game_state_from_file(const string&in filename) { incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0)); incense_burning = get_bool(saveData, "incense_burning", false); if (incense_hours_remaining > 0) incense_burning = true; + petActive = get_bool(saveData, "pet_active", false); + string loadedPetSound; + petSoundPath = ""; + if (saveData.get("pet_sound_path", loadedPetSound)) { + petSoundPath = loadedPetSound; + } + string loadedPetType; + petType = ""; + if (saveData.get("pet_type", loadedPetType)) { + petType = loadedPetType; + } + string loadedPetGender; + petGender = ""; + if (saveData.get("pet_gender", loadedPetGender)) { + petGender = loadedPetGender; + } + petLoyalty = int(get_number(saveData, "pet_loyalty", 0)); + if (!petActive) { + petSoundPath = ""; + petType = ""; + petGender = ""; + petLoyalty = 0; + } + if (petActive && petSoundPath != "" && !file_exists(petSoundPath)) { + petActive = false; + petSoundPath = ""; + petType = ""; + petGender = ""; + petLoyalty = 0; + } + if (petActive) { + petAttackTimer.restart(); + petRetrieveTimer.restart(); + } if (x < 0) x = 0; if (x >= MAP_SIZE) x = MAP_SIZE - 1; diff --git a/src/text_reader.nvgt b/src/text_reader.nvgt index 09edfff..8a69db1 100644 --- a/src/text_reader.nvgt +++ b/src/text_reader.nvgt @@ -44,6 +44,7 @@ string text_reader(string content, string title = "Text Reader", bool readonly = 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)) { diff --git a/src/time_system.nvgt b/src/time_system.nvgt index 11323dd..7345b68 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -139,7 +139,7 @@ void attempt_player_item_break_check() { get_breakable_personal_item_types(breakableItems); if (breakableItems.length() == 0) return; - int roll = random(1, 100); + int roll = random(1, PLAYER_ITEM_BREAK_ROLL_MAX); int checkChance = int(playerItemBreakChance); // Floor for comparison if (roll <= checkChance) { int pickIndex = random(0, int(breakableItems.length()) - 1); @@ -610,6 +610,11 @@ void end_invasion() { void transition_bandits_to_wandering() { for (uint i = 0; i < bandits.length(); i++) { + if (bandits[i].position < bandits[i].home_start) { + bandits[i].home_start = bandits[i].position; + } else if (bandits[i].position > bandits[i].home_end) { + bandits[i].home_end = bandits[i].position; + } bandits[i].behavior_state = "wandering"; bandits[i].wander_direction = random(-1, 1); bandits[i].wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); @@ -745,6 +750,7 @@ void update_time() { if (current_hour % 8 == 0) { consume_food_for_residents(); } + handle_pet_hourly_update(current_hour); if (current_hour == 18 && !sun_setting_warned) { notify("The sun is setting."); @@ -870,7 +876,35 @@ string get_time_string() { period = "pm"; } - return display_hour + " oclock " + period + " day " + current_day; + string result = display_hour + " oclock " + period + " day " + current_day; + string[] conditions; + if (is_daytime) { + if (weather_state == WEATHER_CLEAR) { + conditions.insert_last("Sunny"); + } else { + conditions.insert_last("Daylight"); + } + } else { + conditions.insert_last("Dark"); + } + if (weather_state == WEATHER_WINDY) { + conditions.insert_last("Windy"); + } else if (weather_state == WEATHER_RAINY) { + conditions.insert_last("Raining"); + } else if (weather_state == WEATHER_STORMY) { + conditions.insert_last("Storming"); + } + if (!is_daytime) { + conditions.insert_last("Moon and stars"); + } + if (conditions.length() > 0) { + string condition_text = conditions[0]; + for (uint i = 1; i < conditions.length(); i++) { + condition_text += ", " + conditions[i]; + } + result += ". " + condition_text; + } + return result; } void check_ambience_transition() {