From 43b24b6d1143d84faa5131673c74967c54b18f14 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 25 Jan 2026 21:10:38 -0500 Subject: [PATCH] Lots of changes, forgot to add the heal scroll sound. Sounds for when enemies come in and go out of range. Menu filtering added. --- draugnorak.nvgt | 1 + sounds/actions/heal_scroll.ogg | 3 ++ sounds/enemies/bandit_dies.ogg | 3 ++ sounds/enemies/enter_range.ogg | 3 ++ sounds/enemies/exit_range.ogg | 3 ++ sounds/enemies/goblin1.ogg | 3 ++ sounds/enemies/goblin_dies.ogg | 3 ++ src/base_system.nvgt | 6 ++- src/combat.nvgt | 2 +- src/constants.nvgt | 1 + src/crafting/craft_barricade.nvgt | 8 +-- src/creature_audio.nvgt | 53 +++++++++++++++++++- src/creature_template.nvgt | 4 +- src/enemies/bandit.nvgt | 59 +++++++++++++++++++--- src/enemies/flying_creatures.nvgt | 22 +++++++++ src/enemies/ground_game.nvgt | 16 ++++++ src/enemies/undead.nvgt | 18 ++++++- src/inventory.nvgt | 1 + src/inventory_items.nvgt | 48 +++++++++++++++++- src/item_registry.nvgt | 19 +++++++ src/menus/action_menu.nvgt | 43 +++++++++++++--- src/menus/altar_menu.nvgt | 63 ++++++++++++++++++------ src/menus/base_info.nvgt | 33 ++++++++++--- src/menus/equipment_menu.nvgt | 46 ++++++++++++----- src/menus/inventory_core.nvgt | 73 +++++++++++++++++++++------ src/menus/menu_utils.nvgt | 73 +++++++++++++++++++++++++++ src/menus/storage_menu.nvgt | 82 ++++++++++++++++++------------- src/save_system.nvgt | 43 ++++++++++++++-- src/time_system.nvgt | 58 ++++++++++++++++++---- src/weapon_range_audio.nvgt | 60 ++++++++++++++++++++++ 30 files changed, 731 insertions(+), 119 deletions(-) create mode 100644 sounds/actions/heal_scroll.ogg create mode 100644 sounds/enemies/bandit_dies.ogg create mode 100644 sounds/enemies/enter_range.ogg create mode 100644 sounds/enemies/exit_range.ogg create mode 100644 sounds/enemies/goblin1.ogg create mode 100644 sounds/enemies/goblin_dies.ogg create mode 100644 src/weapon_range_audio.nvgt diff --git a/draugnorak.nvgt b/draugnorak.nvgt index 2b56f0a..cda8bf3 100644 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -164,6 +164,7 @@ void main() update_bandits(); update_boars(); update_flying_creatures(); + update_weapon_range_audio_all(); update_world_drops(); update_blessings(); update_notifications(); diff --git a/sounds/actions/heal_scroll.ogg b/sounds/actions/heal_scroll.ogg new file mode 100644 index 0000000..06d0fe0 --- /dev/null +++ b/sounds/actions/heal_scroll.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cefd712150975693c8ae2de08c01b246ed0711c05f2d3382f0e8201c97823edc +size 32515 diff --git a/sounds/enemies/bandit_dies.ogg b/sounds/enemies/bandit_dies.ogg new file mode 100644 index 0000000..c527dc0 --- /dev/null +++ b/sounds/enemies/bandit_dies.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48a1b6aa37df1d0a179b2439b90fc02dde9b8188da9286d6b237a3b2b5e6e343 +size 10607 diff --git a/sounds/enemies/enter_range.ogg b/sounds/enemies/enter_range.ogg new file mode 100644 index 0000000..6f09c20 --- /dev/null +++ b/sounds/enemies/enter_range.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffafd1f8a3d3b8d97635c46344601b557949127ccd8a00959c1032cdc6a2e8f7 +size 5644 diff --git a/sounds/enemies/exit_range.ogg b/sounds/enemies/exit_range.ogg new file mode 100644 index 0000000..9a0c2c5 --- /dev/null +++ b/sounds/enemies/exit_range.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:000f31779a3a69aa1c78022baddf88ab6fd1d3ecf48f1f6c4a7a349cb9058630 +size 5350 diff --git a/sounds/enemies/goblin1.ogg b/sounds/enemies/goblin1.ogg new file mode 100644 index 0000000..456c167 --- /dev/null +++ b/sounds/enemies/goblin1.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b920e6eaf493b5acd3c15cb09349f2f1f30fcbbd1a4d58dcd1a1859502a94b68 +size 16202 diff --git a/sounds/enemies/goblin_dies.ogg b/sounds/enemies/goblin_dies.ogg new file mode 100644 index 0000000..ea5de02 --- /dev/null +++ b/sounds/enemies/goblin_dies.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48df0bb49e7de17cfb3e532bbed2f2e356618059a8b135b679068a705d9dca34 +size 19943 diff --git a/src/base_system.nvgt b/src/base_system.nvgt index 4ef32a6..eae6fea 100644 --- a/src/base_system.nvgt +++ b/src/base_system.nvgt @@ -303,7 +303,11 @@ void attempt_resident_sling_defense() { } // Play hit sound on enemy - play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, targetPos, ZOMBIE_SOUND_VOLUME_STEP); + if (targetIsBandit) { + play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, targetPos, BANDIT_SOUND_VOLUME_STEP); + } else { + play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, targetPos, ZOMBIE_SOUND_VOLUME_STEP); + } } void process_daily_weapon_breakage() { diff --git a/src/combat.nvgt b/src/combat.nvgt index c1ad6d2..2d9d60e 100644 --- a/src/combat.nvgt +++ b/src/combat.nvgt @@ -171,7 +171,7 @@ void release_sling_attack(int player_x) { hit_bandit = true; break; } - + // Then check for boar GroundGame@ boar = get_boar_at(check_x); if (boar != null) { diff --git a/src/constants.nvgt b/src/constants.nvgt index 81e7cab..4a5694f 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -119,6 +119,7 @@ const int INVASION_DURATION_HOURS = 1; const int BANDIT_DETECTION_RADIUS = 5; const int BANDIT_WANDER_DIRECTION_CHANGE_MIN = 3000; const int BANDIT_WANDER_DIRECTION_CHANGE_MAX = 8000; +const int INVADER_SOUND_VARIANTS_MAX = 5; // Audio ranges and volume falloff // Formula: final_volume = start_volume - (volume_step × distance) diff --git a/src/crafting/craft_barricade.nvgt b/src/crafting/craft_barricade.nvgt index f1f3632..a3fb180 100644 --- a/src/crafting/craft_barricade.nvgt +++ b/src/crafting/craft_barricade.nvgt @@ -155,7 +155,7 @@ void reinforce_barricade_max_with_sticks() { int to_do = (max_reinforcements < can_afford) ? max_reinforcements : can_afford; int total_cost = to_do * BARRICADE_STICK_COST; - simulate_crafting(total_cost); + simulate_crafting(to_do); add_personal_count(ITEM_STICKS, -total_cost); barricade_health += (to_do * BARRICADE_STICK_HEALTH); if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; @@ -181,7 +181,7 @@ void reinforce_barricade_max_with_vines() { int to_do = (max_reinforcements < can_afford) ? max_reinforcements : can_afford; int total_cost = to_do * BARRICADE_VINE_COST; - simulate_crafting(total_cost); + simulate_crafting(to_do); add_personal_count(ITEM_VINES, -total_cost); barricade_health += (to_do * BARRICADE_VINE_HEALTH); if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; @@ -207,7 +207,7 @@ void reinforce_barricade_max_with_log() { int to_do = (max_reinforcements < can_afford) ? max_reinforcements : can_afford; int total_cost = to_do * BARRICADE_LOG_COST; - simulate_crafting(total_cost); + simulate_crafting(to_do); add_personal_count(ITEM_LOGS, -total_cost); barricade_health += (to_do * BARRICADE_LOG_HEALTH); if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; @@ -233,7 +233,7 @@ void reinforce_barricade_max_with_stones() { int to_do = (max_reinforcements < can_afford) ? max_reinforcements : can_afford; int total_cost = to_do * BARRICADE_STONE_COST; - simulate_crafting(total_cost); + simulate_crafting(to_do); add_personal_count(ITEM_STONES, -total_cost); barricade_health += (to_do * BARRICADE_STONE_HEALTH); if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; diff --git a/src/creature_audio.nvgt b/src/creature_audio.nvgt index f43dde3..68f075c 100644 --- a/src/creature_audio.nvgt +++ b/src/creature_audio.nvgt @@ -17,7 +17,7 @@ // - play_creature_voice() for periodic sounds (bleats, moos, neighs) - returns handle, store it! // - play_creature_footstep() when creature moves // - play_creature_attack_sound() when creature attacks player/barricade -// - play_creature_death_sound() when creature dies +// - play_creature_death_sounds() when creature dies (adds optional *_dies.ogg) // - play_creature_hit_sound() when player damages creature // // 3. IMPORTANT: When creature dies, stop its sound before playing death sound: @@ -25,7 +25,7 @@ // p.destroy_sound(creature.sound_handle); // creature.sound_handle = -1; // } -// play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, volume_step); +// play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", creature.voice_sound, x, pos, volume_step); // // 4. All sounds automatically use consistent panning/volume falloff // @@ -79,6 +79,55 @@ void play_creature_death_sound(string sound_file, int listener_x, int creature_x play_1d_with_volume_step(sound_file, listener_x, creature_x, false, volume_step); } +string get_creature_death_sound_from_alert(string alert_sound) +{ + if (alert_sound == "") { + return ""; + } + + string[] parts = alert_sound.split("/"); + string filename = alert_sound; + if (parts.length() > 0) { + filename = parts[parts.length() - 1]; + } + + int dot_index = filename.find("."); + if (dot_index > 0) { + filename = filename.substr(0, dot_index); + } + + // Strip trailing digits (bandit1 -> bandit, goblin2 -> goblin) + while (filename.length() > 0) { + string last_char = filename.substr(filename.length() - 1, 1); + if (last_char >= "0" && last_char <= "9") { + filename = filename.substr(0, filename.length() - 1); + } else { + break; + } + } + + if (filename == "") { + return ""; + } + + string death_sound = "sounds/enemies/" + filename + "_dies.ogg"; + if (!file_exists(death_sound)) { + return ""; + } + + return death_sound; +} + +void play_creature_death_sounds(string default_sound, string alert_sound, int listener_x, int creature_x, float volume_step = CREATURE_DEFAULT_VOLUME_STEP) +{ + play_creature_death_sound(default_sound, listener_x, creature_x, volume_step); + + string death_sound = get_creature_death_sound_from_alert(alert_sound); + if (death_sound != "" && death_sound != default_sound) { + play_creature_death_sound(death_sound, listener_x, creature_x, volume_step); + } +} + // Plays a creature hit/damage sound (when player damages the creature) void play_creature_hit_sound(string sound_file, int listener_x, int creature_x, float volume_step = CREATURE_DEFAULT_VOLUME_STEP) { diff --git a/src/creature_template.nvgt b/src/creature_template.nvgt index 16e31f6..91e2eda 100644 --- a/src/creature_template.nvgt +++ b/src/creature_template.nvgt @@ -217,7 +217,7 @@ bool damage_goblin_at(int pos, int damage) { goblins[i].sound_handle = -1; } // REQUIRED: Use creature_audio for death sound - play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, GOBLIN_SOUND_VOLUME_STEP); + play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", goblins[i].voice_sound, x, pos, GOBLIN_SOUND_VOLUME_STEP); goblins.remove_at(i); } return true; @@ -307,7 +307,7 @@ play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, GOB [ ] Update: play_creature_voice() with stored handle [ ] Movement: play_creature_footstep() [ ] Attacks: play_creature_attack_sound() for weapons - [ ] Death: Stop sound_handle BEFORE play_creature_death_sound() + [ ] Death: Stop sound_handle BEFORE play_creature_death_sounds() [ ] Player damages creature: play_creature_hit_sound() Functions: diff --git a/src/enemies/bandit.nvgt b/src/enemies/bandit.nvgt index 492f169..bbeb5d2 100644 --- a/src/enemies/bandit.nvgt +++ b/src/enemies/bandit.nvgt @@ -3,12 +3,44 @@ string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"}; +string[] get_invader_sound_list(const string&in invader_type) { + string[] sounds; + if (invader_type == "") { + return sounds; + } + + for (int i = 1; i <= INVADER_SOUND_VARIANTS_MAX; i++) { + string sound_file = "sounds/enemies/" + invader_type + i + ".ogg"; + if (file_exists(sound_file)) { + sounds.insert_last(sound_file); + } + } + + return sounds; +} + +string pick_invader_alert_sound(const string&in invader_type) { + string[] sounds = get_invader_sound_list(invader_type); + if (sounds.length() == 0) { + sounds = bandit_sounds; + } + + if (sounds.length() == 0) { + return ""; + } + + int sound_index = random(0, sounds.length() - 1); + return sounds[sound_index]; +} + class Bandit { int position; int health; string alert_sound; + string invader_type; string weapon_type; // "spear" or "axe" int sound_handle; + bool in_weapon_range; timer move_timer; timer attack_timer; int move_interval; @@ -19,15 +51,15 @@ class Bandit { timer wander_direction_timer; int wander_direction_change_interval; - Bandit(int pos, int expansion_start, int expansion_end) { + Bandit(int pos, int expansion_start, int expansion_end, string invader = "bandit") { // Spawn somewhere in the expanded area position = random(expansion_start, expansion_end); health = BANDIT_HEALTH; sound_handle = -1; + invader_type = invader; // Choose random alert sound - int sound_index = random(0, bandit_sounds.length() - 1); - alert_sound = bandit_sounds[sound_index]; + alert_sound = pick_invader_alert_sound(invader_type); // Choose random weapon weapon_type = (random(0, 1) == 0) ? "spear" : "axe"; @@ -43,14 +75,28 @@ class Bandit { wander_direction = 0; wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); wander_direction_timer.restart(); + in_weapon_range = false; } } Bandit@[] bandits; +void update_bandit_weapon_range_audio() { + for (uint i = 0; i < bandits.length(); i++) { + update_weapon_range_audio(bandits[i].position, bandits[i].in_weapon_range); + } +} +bool bandit_range_audio_registered = false; + +void ensure_bandit_range_audio_registration() { + if (bandit_range_audio_registered) return; + bandit_range_audio_registered = register_weapon_range_audio_callback(@update_bandit_weapon_range_audio); +} + void clear_bandits() { if (bandits.length() == 0) return; for (uint i = 0; i < bandits.length(); i++) { + force_weapon_range_exit(bandits[i].position, bandits[i].in_weapon_range); if (bandits[i].sound_handle != -1) { p.destroy_sound(bandits[i].sound_handle); bandits[i].sound_handle = -1; @@ -68,7 +114,7 @@ Bandit@ get_bandit_at(int pos) { return null; } -void spawn_bandit(int expansion_start, int expansion_end) { +void spawn_bandit(int expansion_start, int expansion_end, const string&in invader_type = "bandit") { int spawn_x = -1; for (int attempts = 0; attempts < 20; attempts++) { int candidate = random(expansion_start, expansion_end); @@ -81,7 +127,7 @@ void spawn_bandit(int expansion_start, int expansion_end) { spawn_x = random(expansion_start, expansion_end); } - Bandit@ b = Bandit(spawn_x, expansion_start, expansion_end); + Bandit@ b = Bandit(spawn_x, expansion_start, expansion_end, invader_type); bandits.insert_last(b); // Play looping sound that follows the bandit int[] areaStarts; @@ -289,6 +335,7 @@ void update_bandit(Bandit@ bandit, bool audio_active) { } void update_bandits() { + ensure_bandit_range_audio_registration(); int[] areaStarts; int[] areaEnds; get_active_audio_areas(areaStarts, areaEnds); @@ -309,7 +356,7 @@ bool damage_bandit_at(int pos, int damage) { p.destroy_sound(bandits[i].sound_handle); bandits[i].sound_handle = -1; } - play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, BANDIT_SOUND_VOLUME_STEP); + play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", bandits[i].alert_sound, x, pos, BANDIT_SOUND_VOLUME_STEP); bandits.remove_at(i); } return true; diff --git a/src/enemies/flying_creatures.nvgt b/src/enemies/flying_creatures.nvgt index 7691b03..e979cf3 100644 --- a/src/enemies/flying_creatures.nvgt +++ b/src/enemies/flying_creatures.nvgt @@ -44,6 +44,7 @@ class FlyingCreature { bool fading_out; bool ready_to_remove; timer fade_timer; + bool in_weapon_range; FlyingCreature(string type, int pos, int home_start, int home_end, FlyingCreatureConfig@ cfg) { position = pos; @@ -65,10 +66,29 @@ class FlyingCreature { next_move_delay = random(cfg.move_interval_min, cfg.move_interval_max); fading_out = false; ready_to_remove = false; + in_weapon_range = false; } } FlyingCreature@[] flying_creatures; +void update_flying_creature_weapon_range_audio() { + for (uint i = 0; i < flying_creatures.length(); i++) { + if (flying_creatures[i].state == "flying") { + update_weapon_range_audio(flying_creatures[i].position, flying_creatures[i].in_weapon_range); + } else if (flying_creatures[i].state == "fading") { + force_weapon_range_exit(flying_creatures[i].position, flying_creatures[i].in_weapon_range); + } else { + flying_creatures[i].in_weapon_range = false; + } + } +} +bool flying_creature_range_audio_registered = false; + +void ensure_flying_creature_range_audio_registration() { + if (flying_creature_range_audio_registered) return; + flying_creature_range_audio_registered = register_weapon_range_audio_callback(@update_flying_creature_weapon_range_audio); +} + void init_flying_creature_configs() { flying_creature_configs.resize(0); @@ -116,6 +136,7 @@ FlyingCreatureConfig@ get_flying_creature_config_by_drop_type(string drop_type) void clear_flying_creatures() { for (uint i = 0; i < flying_creatures.length(); i++) { + force_weapon_range_exit(flying_creatures[i].position, flying_creatures[i].in_weapon_range); if (flying_creatures[i].sound_handle != -1) { p.destroy_sound(flying_creatures[i].sound_handle); flying_creatures[i].sound_handle = -1; @@ -357,6 +378,7 @@ void update_flying_creature(FlyingCreature@ creature, bool audio_active) { } void update_flying_creatures() { + ensure_flying_creature_range_audio_registration(); int[] areaStarts; int[] areaEnds; get_active_audio_areas(areaStarts, areaEnds); diff --git a/src/enemies/ground_game.nvgt b/src/enemies/ground_game.nvgt index 0bfe196..c8bde25 100644 --- a/src/enemies/ground_game.nvgt +++ b/src/enemies/ground_game.nvgt @@ -16,6 +16,7 @@ class GroundGame { int area_end; int wander_direction; // -1, 0, 1 string animal_type; // "boar", future: "mountain_goat", "ram", etc. + bool in_weapon_range; GroundGame(int pos, int start, int end, string type = "boar") { position = pos; @@ -33,14 +34,28 @@ class GroundGame { attack_timer.restart(); next_move_delay = random(BOAR_MOVE_INTERVAL_MIN, BOAR_MOVE_INTERVAL_MAX); + in_weapon_range = false; } } GroundGame@[] ground_games; +void update_ground_game_weapon_range_audio() { + for (uint i = 0; i < ground_games.length(); i++) { + update_weapon_range_audio(ground_games[i].position, ground_games[i].in_weapon_range); + } +} +bool ground_game_range_audio_registered = false; + +void ensure_ground_game_range_audio_registration() { + if (ground_game_range_audio_registered) return; + ground_game_range_audio_registered = register_weapon_range_audio_callback(@update_ground_game_weapon_range_audio); +} + void clear_ground_games() { if (ground_games.length() == 0) return; for (uint i = 0; i < ground_games.length(); i++) { + force_weapon_range_exit(ground_games[i].position, ground_games[i].in_weapon_range); if (ground_games[i].sound_handle != -1) { p.destroy_sound(ground_games[i].sound_handle); ground_games[i].sound_handle = -1; @@ -189,6 +204,7 @@ void update_ground_game(GroundGame@ game, bool audio_active) { } void update_ground_games() { + ensure_ground_game_range_audio_registration(); int[] areaStarts; int[] areaEnds; get_active_audio_areas(areaStarts, areaEnds); diff --git a/src/enemies/undead.nvgt b/src/enemies/undead.nvgt index 1d2d299..05ccf14 100644 --- a/src/enemies/undead.nvgt +++ b/src/enemies/undead.nvgt @@ -9,6 +9,7 @@ class Undead { string undead_type; // "zombie", future: "vampire", "ghost", etc. string voice_sound; int sound_handle; + bool in_weapon_range; timer move_timer; timer attack_timer; @@ -19,16 +20,30 @@ class Undead { int sound_index = random(0, undead_zombie_sounds.length() - 1); voice_sound = undead_zombie_sounds[sound_index]; sound_handle = -1; + in_weapon_range = false; move_timer.restart(); attack_timer.restart(); } } Undead@[] undeads; +void update_undead_weapon_range_audio() { + for (uint i = 0; i < undeads.length(); i++) { + update_weapon_range_audio(undeads[i].position, undeads[i].in_weapon_range); + } +} +bool undead_range_audio_registered = false; + +void ensure_undead_range_audio_registration() { + if (undead_range_audio_registered) return; + undead_range_audio_registered = register_weapon_range_audio_callback(@update_undead_weapon_range_audio); +} + void clear_undeads() { if (undeads.length() == 0) return; for (uint i = 0; i < undeads.length(); i++) { + force_weapon_range_exit(undeads[i].position, undeads[i].in_weapon_range); if (undeads[i].sound_handle != -1) { p.destroy_sound(undeads[i].sound_handle); undeads[i].sound_handle = -1; @@ -191,6 +206,7 @@ void update_undead(Undead@ undead, bool audio_active) { } void update_undeads() { + ensure_undead_range_audio_registration(); if (is_daytime) { clear_undeads(); return; @@ -227,7 +243,7 @@ bool damage_undead_at(int pos, int damage) { p.destroy_sound(undeads[i].sound_handle); undeads[i].sound_handle = -1; } - play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, ZOMBIE_SOUND_VOLUME_STEP); + play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", undeads[i].voice_sound, x, pos, ZOMBIE_SOUND_VOLUME_STEP); undeads.remove_at(i); } return true; diff --git a/src/inventory.nvgt b/src/inventory.nvgt index 58be70a..b28524f 100644 --- a/src/inventory.nvgt +++ b/src/inventory.nvgt @@ -1,6 +1,7 @@ // Inventory module includes #include "src/item_registry.nvgt" #include "src/inventory_items.nvgt" +#include "src/weapon_range_audio.nvgt" #include "src/runes/rune_data.nvgt" #include "src/runes/rune_effects.nvgt" #include "src/inventory_menus.nvgt" diff --git a/src/inventory_items.nvgt b/src/inventory_items.nvgt index 94da265..0d04afb 100644 --- a/src/inventory_items.nvgt +++ b/src/inventory_items.nvgt @@ -39,12 +39,18 @@ int equipped_feet = EQUIP_NONE; // Quick slots int[] quick_slots; +int[] item_count_slots; void reset_quick_slots() { quick_slots.resize(10); for (uint i = 0; i < quick_slots.length(); i++) { quick_slots[i] = -1; } + + item_count_slots.resize(10); + for (uint i = 0; i < item_count_slots.length(); i++) { + item_count_slots[i] = -1; + } } int get_personal_stack_limit() { @@ -218,6 +224,37 @@ int get_quick_slot_key() { return -1; } +int get_item_count_for_type(int item_type) { + if (is_runed_item_type(item_type)) { + int equip_type = 0; + int rune_type = 0; + decode_runed_item_type(item_type, equip_type, rune_type); + return get_runed_item_count(equip_type, rune_type); + } + return get_personal_count(item_type); +} + +string get_item_count_label(int item_type, int count) { + if (is_runed_item_type(item_type)) { + int equip_type = 0; + int rune_type = 0; + decode_runed_item_type(item_type, equip_type, rune_type); + string name = "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); + return count + " " + name; + } + return count + " " + get_item_label_for_count(item_type, count); +} + +string get_item_count_binding_name(int item_type) { + if (is_runed_item_type(item_type)) { + int equip_type = 0; + int rune_type = 0; + decode_runed_item_type(item_type, equip_type, rune_type); + return "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); + } + return get_item_display_name(item_type); +} + bool try_consume_heal_scroll() { if (player_health > 0) return false; if (get_personal_count(ITEM_HEAL_SCROLL) <= 0) return false; @@ -236,7 +273,16 @@ void activate_quick_slot(int slot_index) { int equip_type = quick_slots[slot_index]; if (equip_type < 0) { - speak_with_history("No item bound to slot " + slot_index + ".", true); + int item_type = -1; + if (slot_index >= 0 && slot_index < int(item_count_slots.length())) { + item_type = item_count_slots[slot_index]; + } + if (item_type != -1) { + int count = get_item_count_for_type(item_type); + speak_with_history(get_item_count_label(item_type, count) + ".", true); + } else { + speak_with_history("No item bound to slot " + slot_index + ".", true); + } return; } diff --git a/src/item_registry.nvgt b/src/item_registry.nvgt index 4f12664..3caca8d 100644 --- a/src/item_registry.nvgt +++ b/src/item_registry.nvgt @@ -314,3 +314,22 @@ string get_item_label_for_count(int item_type, int count) { } return get_item_label(item_type); } + +// Encode runed item info into a negative item type +// Format: -1000 - (equip_type * 10) - rune_type +// This allows us to extract both equip_type and rune_type from a single int +int encode_runed_item_type(int equip_type, int rune_type) { + return -1000 - (equip_type * 10) - rune_type; +} + +// Decode a runed item type back to equip_type and rune_type +void decode_runed_item_type(int encoded, int& out equip_type, int& out rune_type) { + int decoded = -(encoded + 1000); + equip_type = decoded / 10; + rune_type = decoded % 10; +} + +// Check if an item_type represents a runed item +bool is_runed_item_type(int item_type) { + return item_type <= -1000; +} diff --git a/src/menus/action_menu.nvgt b/src/menus/action_menu.nvgt index 3c0a494..d88f00d 100644 --- a/src/menus/action_menu.nvgt +++ b/src/menus/action_menu.nvgt @@ -256,6 +256,10 @@ void run_action_menu(int x) { options.insert_last("Check fishing pole"); action_types.insert_last(7); } + string filter_text = ""; + int[] filtered_indices; + string[] filtered_options; + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); while(true) { wait(5); @@ -265,20 +269,40 @@ void run_action_menu(int x) { 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)) { - selection++; - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection++; + if (selection >= int(filtered_options.length())) selection = 0; + speak_with_history(filtered_options[selection], true); + } } if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection--; + if (selection < 0) selection = int(filtered_options.length()) - 1; + speak_with_history(filtered_options[selection], true); + } } if (key_pressed(KEY_RETURN)) { - int action = action_types[selection]; + if (filtered_indices.length() == 0) { + continue; + } + int action = action_types[filtered_indices[selection]]; if (action == 0) { try_place_snare(x); } else if (action == 1) { @@ -300,7 +324,10 @@ void run_action_menu(int x) { } if (key_pressed(KEY_TAB)) { - int action = action_types[selection]; + if (filtered_indices.length() == 0) { + continue; + } + int action = action_types[filtered_indices[selection]]; if (action == 0) { speak_with_history("Can't do that.", true); } else if (action == 1) { diff --git a/src/menus/altar_menu.nvgt b/src/menus/altar_menu.nvgt index 92436ea..1c9a69e 100644 --- a/src/menus/altar_menu.nvgt +++ b/src/menus/altar_menu.nvgt @@ -81,6 +81,10 @@ void run_altar_menu() { string[] options; int[] item_types; build_personal_inventory_options(options, item_types); + string filter_text = ""; + int[] filtered_indices; + string[] filtered_options; + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); while(true) { wait(5); @@ -90,30 +94,61 @@ void run_altar_menu() { 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)) { - selection++; - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection++; + if (selection >= int(filtered_options.length())) selection = 0; + speak_with_history(filtered_options[selection], true); + } } if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection--; + if (selection < 0) selection = int(filtered_options.length()) - 1; + speak_with_history(filtered_options[selection], true); + } } if (key_pressed(KEY_RETURN)) { - sacrifice_item(item_types[selection]); - build_personal_inventory_options(options, item_types); - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_indices.length() > 0) { + sacrifice_item(item_types[filtered_indices[selection]]); + build_personal_inventory_options(options, item_types); + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); + if (selection >= int(filtered_options.length())) selection = 0; + if (filtered_options.length() > 0) { + speak_with_history(filtered_options[selection], true); + } else if (filter_text.length() > 0) { + speak_with_history("No matches for " + filter_text + ".", true); + } + } } if (key_pressed(KEY_TAB)) { - sacrifice_item_max(item_types[selection]); - build_personal_inventory_options(options, item_types); - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_indices.length() > 0) { + sacrifice_item_max(item_types[filtered_indices[selection]]); + build_personal_inventory_options(options, item_types); + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); + if (selection >= int(filtered_options.length())) selection = 0; + if (filtered_options.length() > 0) { + speak_with_history(filtered_options[selection], true); + } else if (filter_text.length() > 0) { + speak_with_history("No matches for " + filter_text + ".", true); + } + } } } } diff --git a/src/menus/base_info.nvgt b/src/menus/base_info.nvgt index c2d3a50..2e87b4d 100644 --- a/src/menus/base_info.nvgt +++ b/src/menus/base_info.nvgt @@ -43,6 +43,10 @@ void run_base_info_menu() { if (world_stables.length() > 0) options.insert_last("Stable built"); else options.insert_last("Stable not built"); + string filter_text = ""; + int[] filtered_indices; + string[] filtered_options; + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); while(true) { wait(5); @@ -52,16 +56,33 @@ void run_base_info_menu() { 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)) { - selection++; - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection++; + if (selection >= int(filtered_options.length())) selection = 0; + speak_with_history(filtered_options[selection], true); + } } if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection--; + if (selection < 0) selection = int(filtered_options.length()) - 1; + speak_with_history(filtered_options[selection], true); + } } } } diff --git a/src/menus/equipment_menu.nvgt b/src/menus/equipment_menu.nvgt index 7fe5801..434edfb 100644 --- a/src/menus/equipment_menu.nvgt +++ b/src/menus/equipment_menu.nvgt @@ -145,6 +145,10 @@ void run_equipment_menu() { rune_types.insert_last(RUNE_SWIFTNESS); } } + string filter_text = ""; + int[] filtered_indices; + string[] filtered_options; + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); while(true) { wait(5); @@ -154,30 +158,50 @@ void run_equipment_menu() { 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)) { - selection++; - if (selection >= int(options.length())) selection = 0; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection++; + if (selection >= int(filtered_options.length())) selection = 0; + speak_with_history(filtered_options[selection], true); + } } if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = int(options.length()) - 1; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection--; + if (selection < 0) selection = int(filtered_options.length()) - 1; + speak_with_history(filtered_options[selection], true); + } } int slot_index = get_quick_slot_key(); - if (slot_index != -1) { - int equip_type = equipment_types[selection]; - int rune_type = rune_types[selection]; + if (slot_index != -1 && filtered_indices.length() > 0) { + int equip_type = equipment_types[filtered_indices[selection]]; + int rune_type = rune_types[filtered_indices[selection]]; quick_slots[slot_index] = equip_type; string name = get_full_equipment_name(equip_type, rune_type); speak_with_history(name + " set to slot " + slot_index + ".", true); } if (key_pressed(KEY_RETURN)) { - int equip_type = equipment_types[selection]; - int rune_type = rune_types[selection]; + if (filtered_indices.length() == 0) { + continue; + } + int equip_type = equipment_types[filtered_indices[selection]]; + int rune_type = rune_types[filtered_indices[selection]]; string name = get_full_equipment_name(equip_type, rune_type); if (is_runed_item_equipped(equip_type, rune_type)) { diff --git a/src/menus/inventory_core.nvgt b/src/menus/inventory_core.nvgt index c2b5649..780e841 100644 --- a/src/menus/inventory_core.nvgt +++ b/src/menus/inventory_core.nvgt @@ -105,6 +105,10 @@ void run_inventory_menu(bool allow_deposit) { string[] options; int[] item_types; build_personal_inventory_options(options, item_types); + string filter_text = ""; + int[] filtered_indices; + string[] filtered_options; + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); while(true) { wait(5); @@ -114,30 +118,71 @@ void run_inventory_menu(bool allow_deposit) { 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)) { - selection++; - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection++; + if (selection >= int(filtered_options.length())) selection = 0; + speak_with_history(filtered_options[selection], true); + } } if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection--; + if (selection < 0) selection = int(filtered_options.length()) - 1; + speak_with_history(filtered_options[selection], true); + } + } + + int slot_index = get_quick_slot_key(); + if (slot_index != -1 && filtered_indices.length() > 0) { + int item_type = item_types[filtered_indices[selection]]; + if (slot_index >= 0 && slot_index < int(item_count_slots.length())) { + item_count_slots[slot_index] = item_type; + string name = get_item_count_binding_name(item_type); + speak_with_history(name + " count set to slot " + slot_index + ".", true); + } } if (allow_deposit && key_pressed(KEY_RETURN)) { - deposit_item(item_types[selection]); - build_personal_inventory_options(options, item_types); - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_indices.length() > 0) { + deposit_item(item_types[filtered_indices[selection]]); + build_personal_inventory_options(options, item_types); + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); + if (selection >= int(filtered_options.length())) selection = 0; + if (filtered_options.length() > 0) { + speak_with_history(filtered_options[selection], true); + } else if (filter_text.length() > 0) { + speak_with_history("No matches for " + filter_text + ".", true); + } + } } if (allow_deposit && key_pressed(KEY_TAB)) { - deposit_item_max(item_types[selection]); - build_personal_inventory_options(options, item_types); - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_indices.length() > 0) { + deposit_item_max(item_types[filtered_indices[selection]]); + build_personal_inventory_options(options, item_types); + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); + if (selection >= int(filtered_options.length())) selection = 0; + if (filtered_options.length() > 0) { + speak_with_history(filtered_options[selection], true); + } else if (filter_text.length() > 0) { + speak_with_history("No matches for " + filter_text + ".", true); + } + } } } } diff --git a/src/menus/menu_utils.nvgt b/src/menus/menu_utils.nvgt index e32bccb..1ba0e49 100644 --- a/src/menus/menu_utils.nvgt +++ b/src/menus/menu_utils.nvgt @@ -14,6 +14,7 @@ void menu_background_tick() { update_bandits(); update_boars(); update_flying_creatures(); + update_weapon_range_audio_all(); update_world_drops(); update_blessings(); update_notifications(); @@ -79,3 +80,75 @@ string get_base_fire_status() { if (total == 0) return "No fires in base"; return "Fires in base: " + burning + " burning, " + total + " total"; } + +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 ""; +} + +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]); + } + } +} + +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; +} diff --git a/src/menus/storage_menu.nvgt b/src/menus/storage_menu.nvgt index 74f6e3a..f6b5c37 100644 --- a/src/menus/storage_menu.nvgt +++ b/src/menus/storage_menu.nvgt @@ -39,25 +39,6 @@ int prompt_transfer_amount(const string prompt, int max_amount) { return amount; } -// Encode runed item info into a negative item type -// Format: -1000 - (equip_type * 10) - rune_type -// This allows us to extract both equip_type and rune_type from a single int -int encode_runed_item_type(int equip_type, int rune_type) { - return -1000 - (equip_type * 10) - rune_type; -} - -// Decode a runed item type back to equip_type and rune_type -void decode_runed_item_type(int encoded, int& out equip_type, int& out rune_type) { - int decoded = -(encoded + 1000); - equip_type = decoded / 10; - rune_type = decoded % 10; -} - -// Check if an item_type represents a runed item -bool is_runed_item_type(int item_type) { - return item_type <= -1000; -} - void deposit_item(int item_type) { // Handle runed items if (is_runed_item_type(item_type)) { @@ -313,6 +294,10 @@ void run_storage_menu() { string[] options; int[] item_types; build_storage_inventory_options(options, item_types); + string filter_text = ""; + int[] filtered_indices; + string[] filtered_options; + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); while(true) { wait(5); @@ -322,30 +307,61 @@ void run_storage_menu() { 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)) { - selection++; - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection++; + if (selection >= int(filtered_options.length())) selection = 0; + speak_with_history(filtered_options[selection], true); + } } if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - speak_with_history(options[selection], true); + if (filtered_options.length() > 0) { + selection--; + if (selection < 0) selection = int(filtered_options.length()) - 1; + speak_with_history(filtered_options[selection], true); + } } if (key_pressed(KEY_RETURN)) { - withdraw_item(item_types[selection]); - build_storage_inventory_options(options, item_types); - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_indices.length() > 0) { + withdraw_item(item_types[filtered_indices[selection]]); + build_storage_inventory_options(options, item_types); + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); + if (selection >= int(filtered_options.length())) selection = 0; + if (filtered_options.length() > 0) { + speak_with_history(filtered_options[selection], true); + } else if (filter_text.length() > 0) { + speak_with_history("No matches for " + filter_text + ".", true); + } + } } if (key_pressed(KEY_TAB)) { - withdraw_item_max(item_types[selection]); - build_storage_inventory_options(options, item_types); - if (selection >= options.length()) selection = 0; - speak_with_history(options[selection], true); + if (filtered_indices.length() > 0) { + withdraw_item_max(item_types[filtered_indices[selection]]); + build_storage_inventory_options(options, item_types); + apply_menu_filter(filter_text, options, filtered_indices, filtered_options); + if (selection >= int(filtered_options.length())) selection = 0; + if (filtered_options.length() > 0) { + speak_with_history(filtered_options[selection], true); + } else if (filter_text.length() > 0) { + speak_with_history("No matches for " + filter_text + ".", true); + } + } } } } diff --git a/src/save_system.nvgt b/src/save_system.nvgt index 09659f3..df44e32 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -291,7 +291,7 @@ string serialize_stream(WorldStream@ stream) { } string serialize_bandit(Bandit@ bandit) { - return bandit.position + "|" + bandit.health + "|" + bandit.weapon_type + "|" + bandit.behavior_state + "|" + bandit.wander_direction + "|" + bandit.move_interval; + return bandit.position + "|" + bandit.health + "|" + bandit.weapon_type + "|" + bandit.behavior_state + "|" + bandit.wander_direction + "|" + bandit.move_interval + "|" + bandit.invader_type; } string serialize_mountain(MountainRange@ mountain) { @@ -420,6 +420,12 @@ bool save_game_state() { } saveData.set("equipment_quick_slots", join_string_array(quickSlotData)); + string[] itemCountSlotData; + for (uint i = 0; i < item_count_slots.length(); i++) { + itemCountSlotData.insert_last("" + item_count_slots[i]); + } + saveData.set("item_count_slots", join_string_array(itemCountSlotData)); + // Rune system data saveData.set("rune_swiftness_unlocked", rune_swiftness_unlocked); saveData.set("unicorn_boss_defeated", unicorn_boss_defeated); @@ -467,6 +473,7 @@ bool save_game_state() { saveData.set("time_invasion_triggered_today", invasion_triggered_today); saveData.set("time_invasion_roll_done_today", invasion_roll_done_today); saveData.set("time_invasion_scheduled_hour", invasion_scheduled_hour); + saveData.set("time_invasion_enemy_type", invasion_enemy_type); saveData.set("quest_roll_done_today", quest_roll_done_today); string[] questData; for (uint i = 0; i < quest_queue.length(); i++) { @@ -735,6 +742,18 @@ bool load_game_state() { } } + string[] loadedItemCountSlots = get_string_list_or_split(saveData, "item_count_slots"); + uint count_slot_count = loadedItemCountSlots.length(); + if (count_slot_count > item_count_slots.length()) count_slot_count = item_count_slots.length(); + for (uint i = 0; i < count_slot_count; i++) { + int slot_value = parse_int(loadedItemCountSlots[i]); + if (slot_value >= 0 && slot_value < ITEM_COUNT) { + item_count_slots[i] = slot_value; + } else if (is_runed_item_type(slot_value)) { + item_count_slots[i] = slot_value; + } + } + // Load rune system data rune_swiftness_unlocked = get_bool(saveData, "rune_swiftness_unlocked", false); unicorn_boss_defeated = get_bool(saveData, "unicorn_boss_defeated", false); @@ -825,6 +844,15 @@ bool load_game_state() { invasion_triggered_today = get_bool(saveData, "time_invasion_triggered_today", false); invasion_roll_done_today = get_bool(saveData, "time_invasion_roll_done_today", false); invasion_scheduled_hour = int(get_number(saveData, "time_invasion_scheduled_hour", -1)); + string loaded_invasion_type; + if (saveData.get("time_invasion_enemy_type", loaded_invasion_type)) { + invasion_enemy_type = loaded_invasion_type; + } else { + invasion_enemy_type = "bandit"; + } + if (invasion_enemy_type == "") { + invasion_enemy_type = "bandit"; + } if (invasion_chance < 0) invasion_chance = 0; if (invasion_chance > 100) invasion_chance = 100; if (invasion_scheduled_hour < -1) invasion_scheduled_hour = -1; @@ -959,23 +987,28 @@ bool load_game_state() { string state = parts[3]; int wander_dir = parse_int(parts[4]); int move_int = parse_int(parts[5]); + string invader_type = "bandit"; + if (parts.length() >= 7) { + invader_type = parts[6]; + if (invader_type == "") invader_type = "bandit"; + } // Create bandit with dummy expansion area (position will be overridden) - Bandit@ b = Bandit(pos, pos, pos); + Bandit@ b = Bandit(pos, pos, pos, invader_type); b.position = pos; b.health = health; b.weapon_type = weapon; b.behavior_state = state; b.wander_direction = wander_dir; b.move_interval = move_int; + b.invader_type = invader_type; b.wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); b.wander_direction_timer.restart(); b.move_timer.restart(); b.attack_timer.restart(); - // Restore alert sound based on weapon type - int sound_index = random(0, bandit_sounds.length() - 1); - b.alert_sound = bandit_sounds[sound_index]; + // Restore alert sound based on invader type + b.alert_sound = pick_invader_alert_sound(invader_type); bandits.insert_last(b); // Start looping sound for loaded bandit diff --git a/src/time_system.nvgt b/src/time_system.nvgt index d297dba..d0f3a6a 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -29,6 +29,12 @@ int invasion_chance = 25; bool invasion_triggered_today = false; bool invasion_roll_done_today = false; int invasion_scheduled_hour = -1; +string invasion_enemy_type = "bandit"; + +// Invasion mapping: "terrain=enemy" (defaults to bandits when no match) +// Terrain keys: "mountain" for mountain ranges, or regular types like "grass", "snow", "forest", "deep_forest", "stone". +string default_invasion_enemy_type = "bandit"; +string[] invasion_terrain_enemy_map = {"mountain=goblin"}; void init_time() { current_hour = 8; @@ -46,10 +52,37 @@ void init_time() { invasion_triggered_today = false; invasion_roll_done_today = false; invasion_scheduled_hour = -1; + invasion_enemy_type = "bandit"; update_ambience(true); // Force start } -void expand_area() { +string get_invasion_enemy_type_for_terrain(const string&in terrain_type) { + for (uint i = 0; i < invasion_terrain_enemy_map.length(); i++) { + string entry = invasion_terrain_enemy_map[i]; + int eq_pos = entry.find("="); + if (eq_pos <= 0) { + continue; + } + + string key = entry.substr(0, eq_pos); + string value = entry.substr(eq_pos + 1); + if (key == terrain_type && value != "") { + return value; + } + } + return default_invasion_enemy_type; +} + +string get_invasion_enemy_label(const string&in enemy_type) { + if (enemy_type != "") return enemy_type; + return default_invasion_enemy_type; +} + +string get_invasion_enemy_plural(const string&in enemy_type) { + return get_invasion_enemy_label(enemy_type) + "s"; +} + +string expand_area() { // Play invasion sound p.play_stationary("sounds/enemies/invasion.ogg", false); @@ -57,12 +90,13 @@ void expand_area() { int type_roll = random(0, 3); if (type_roll == 0) { expand_mountain(); - } else { - expand_regular_area(); + return "mountain"; } + + return expand_regular_area(); } -void expand_regular_area() { +string expand_regular_area() { // Calculate new area int new_start = MAP_SIZE; int new_end = MAP_SIZE + EXPANSION_SIZE - 1; @@ -120,6 +154,7 @@ void expand_regular_area() { area_expanded_today = true; notify("The area has expanded! New territory discovered to the east."); + return terrain_type; } void expand_mountain() { @@ -147,10 +182,13 @@ void expand_mountain() { } void start_invasion() { - expand_area(); + string expansion_terrain = expand_area(); invasion_active = true; invasion_start_hour = current_hour; - notify("Bandits are invading from the new area!"); + invasion_enemy_type = get_invasion_enemy_type_for_terrain(expansion_terrain); + string source = (expansion_terrain == "mountain") ? "the mountains" : "the new area"; + string enemy_plural = get_invasion_enemy_plural(invasion_enemy_type); + notify(enemy_plural + " are invading from " + source + "!"); } void update_invasion_chance_for_new_day() { @@ -268,7 +306,7 @@ void end_invasion() { invasion_active = false; invasion_start_hour = -1; transition_bandits_to_wandering(); - notify("The bandit invasion has ended."); + notify("The " + get_invasion_enemy_label(invasion_enemy_type) + " invasion has ended."); attempt_resident_recruitment(); } @@ -302,10 +340,10 @@ void manage_bandits_during_invasion() { return; } - // During daytime: if invasion is active, maintain bandit count + // During daytime: if invasion is active, maintain invader count if (invasion_active && expanded_area_start != -1) { while (bandits.length() < BANDIT_MAX_COUNT) { - spawn_bandit(expanded_area_start, expanded_area_end); + spawn_bandit(expanded_area_start, expanded_area_end, invasion_enemy_type); } } // If invasion not active, wandering bandits persist during daytime @@ -448,7 +486,7 @@ void update_time() { // Proactive resident defense with slings attempt_resident_sling_defense(); - // Manage bandits during active invasion + // Manage invasion enemies during active invasion manage_bandits_during_invasion(); } diff --git a/src/weapon_range_audio.nvgt b/src/weapon_range_audio.nvgt new file mode 100644 index 0000000..dcd86e8 --- /dev/null +++ b/src/weapon_range_audio.nvgt @@ -0,0 +1,60 @@ +// Weapon range enter/exit audio system +// Central registry so new enemies only register a callback once + +funcdef void WeaponRangeAudioCallback(); + +WeaponRangeAudioCallback@[] weapon_range_audio_callbacks; +bool weapon_range_audio_initialized = false; + +void init_weapon_range_audio_registry() { + if (weapon_range_audio_initialized) return; + weapon_range_audio_callbacks.resize(0); + weapon_range_audio_initialized = true; +} + +bool register_weapon_range_audio_callback(WeaponRangeAudioCallback@ callback) { + init_weapon_range_audio_registry(); + if (@callback is null) return false; + weapon_range_audio_callbacks.insert_last(callback); + return true; +} + +int get_current_weapon_range() { + if (sling_equipped) return SLING_RANGE; + if (bow_equipped) return BOW_RANGE; + if (spear_equipped) return 1; + if (axe_equipped) return 0; + return -1; +} + +void play_weapon_range_sound(string sound_file, int creature_pos) { + if (!file_exists(sound_file)) return; + // Notification sound: play at player position for consistent perception + play_1d_with_volume_step(sound_file, x, x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); +} + +void update_weapon_range_audio(int creature_pos, bool &inout was_in_range) { + int range = get_current_weapon_range(); + bool in_range = (range >= 0) && (abs(creature_pos - x) <= range); + if (in_range && !was_in_range) { + play_weapon_range_sound("sounds/enemies/enter_range.ogg", creature_pos); + } else if (!in_range && was_in_range) { + play_weapon_range_sound("sounds/enemies/exit_range.ogg", creature_pos); + } + was_in_range = in_range; +} + +void force_weapon_range_exit(int creature_pos, bool &inout was_in_range) { + if (!was_in_range) return; + play_weapon_range_sound("sounds/enemies/exit_range.ogg", creature_pos); + was_in_range = false; +} + +void update_weapon_range_audio_all() { + for (uint i = 0; i < weapon_range_audio_callbacks.length(); i++) { + WeaponRangeAudioCallback@ callback = weapon_range_audio_callbacks[i]; + if (@callback !is null) { + callback(); + } + } +}