From 35e192cb21332bf7f1711950516c22717e8f9ebf Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 22 Jan 2026 19:51:10 -0500 Subject: [PATCH] First adventure added. A bit of sound management improvement. --- .gitignore | 3 + draugnorak.nvgt | 8 +- src/bosses/unicorn/unicorn_boss.nvgt | 26 +++- src/crafting.nvgt | 1 + src/crafting/craft_materials.nvgt | 2 +- src/crafting/craft_runes.nvgt | 190 +++++++++++++++++++++++++++ src/crafting/crafting_core.nvgt | 29 +++- src/enemies/bandit.nvgt | 7 + src/enemies/flying_creatures.nvgt | 1 - src/enemies/undead.nvgt | 7 + src/environment.nvgt | 35 ++++- src/inventory.nvgt | 2 + src/inventory_items.nvgt | 30 ++++- src/menus/action_menu.nvgt | 6 + src/menus/equipment_menu.nvgt | 116 ++++++++++++---- src/player.nvgt | 1 + src/runes/rune_data.nvgt | 158 ++++++++++++++++++++++ src/runes/rune_effects.nvgt | 81 ++++++++++++ src/save_system.nvgt | 50 +++++++ src/time_system.nvgt | 28 ++-- src/weather.nvgt | 21 ++- src/world/mountains.nvgt | 79 +++++++++++ src/world/world_drops.nvgt | 5 + src/world/world_fires.nvgt | 5 + src/world/world_snares.nvgt | 5 + src/world/world_streams.nvgt | 9 +- 26 files changed, 851 insertions(+), 54 deletions(-) create mode 100644 src/crafting/craft_runes.nvgt create mode 100644 src/runes/rune_data.nvgt create mode 100644 src/runes/rune_effects.nvgt diff --git a/.gitignore b/.gitignore index 0e72b76..03ec794 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ lib_windows/ stub/ include/ save.dat +*.bak +*.wav +*.opus .aider* diff --git a/draugnorak.nvgt b/draugnorak.nvgt index d601e00..172b0c5 100644 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -2,7 +2,8 @@ #include "virtual_dialogs.nvgt" // Audio -sound_pool p(100); +// Increased pool size to 300 to prevent sound cutoffs and missing sounds +sound_pool p(300); #include "src/constants.nvgt" #include "src/player.nvgt" @@ -34,6 +35,7 @@ sound_pool p(100); #include "src/creature_audio.nvgt" #include "src/notify.nvgt" #include "src/speech_history.nvgt" +#include "src/text_reader.nvgt" #include "src/bosses/adventure_system.nvgt" int run_main_menu() { @@ -380,7 +382,9 @@ void main() } search_timer.restart(); } - if (shift_down && search_timer.elapsed > 2000 && !searching) + // Apply rune gathering bonus to search time + int search_time = apply_rune_gather_bonus(2000); + if (shift_down && search_timer.elapsed > search_time && !searching) { searching = true; search_delay_timer.restart(); diff --git a/src/bosses/unicorn/unicorn_boss.nvgt b/src/bosses/unicorn/unicorn_boss.nvgt index c4994ea..a203f11 100644 --- a/src/bosses/unicorn/unicorn_boss.nvgt +++ b/src/bosses/unicorn/unicorn_boss.nvgt @@ -396,5 +396,29 @@ void play_unicorn_death_sequence() { void give_unicorn_rewards() { speak_with_history("Victory!", true); - favor += 50; + + // Calculate rewards + int favor_reward = 4 + random(1, 4); // 4 + 1d4 = 5-8 favor + favor += favor_reward; + + // Track boss defeat and unlock rune + unicorn_boss_defeated = true; + bool new_rune = !rune_swiftness_unlocked; + rune_swiftness_unlocked = true; + + // Build rewards display + string[] rewards; + rewards.insert_last("=== Victory Rewards ==="); + rewards.insert_last(""); + rewards.insert_last("The gods are pleased with your victory! " + favor_reward + " favor awarded."); + rewards.insert_last(""); + if (new_rune) { + rewards.insert_last("Learned Rune of Swiftness!"); + rewards.insert_last("You can now engrave equipment with this rune at the crafting menu."); + } else { + rewards.insert_last("You have already mastered the Rune of Swiftness."); + } + + // Display rewards in text reader + text_reader_lines(rewards, "Unicorn Victory", true); } diff --git a/src/crafting.nvgt b/src/crafting.nvgt index 0baf6a9..d665f06 100644 --- a/src/crafting.nvgt +++ b/src/crafting.nvgt @@ -19,3 +19,4 @@ #include "src/crafting/craft_clothing.nvgt" #include "src/crafting/craft_buildings.nvgt" #include "src/crafting/craft_barricade.nvgt" +#include "src/crafting/craft_runes.nvgt" diff --git a/src/crafting/craft_materials.nvgt b/src/crafting/craft_materials.nvgt index 6427875..e9195fd 100644 --- a/src/crafting/craft_materials.nvgt +++ b/src/crafting/craft_materials.nvgt @@ -4,7 +4,7 @@ void run_materials_menu() { int selection = 0; string[] options = { - "Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]", + "Butcher Game [Requires Game, Knife, Fire nearby]", "Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]" }; diff --git a/src/crafting/craft_runes.nvgt b/src/crafting/craft_runes.nvgt new file mode 100644 index 0000000..7a2a2c5 --- /dev/null +++ b/src/crafting/craft_runes.nvgt @@ -0,0 +1,190 @@ +// Rune engraving crafting menu +// Requires: Knife (tool), 1 Clay, 1 Favor +// Must be in base area + +// Get the base equipment name without any rune prefix +string get_base_equipment_name(int equip_type) { + if (equip_type == EQUIP_SPEAR) return "Spear"; + if (equip_type == EQUIP_AXE) return "Stone Axe"; + if (equip_type == EQUIP_SLING) return "Sling"; + if (equip_type == EQUIP_BOW) return "Bow"; + if (equip_type == EQUIP_HAT) return "Skin Hat"; + if (equip_type == EQUIP_GLOVES) return "Skin Gloves"; + if (equip_type == EQUIP_PANTS) return "Skin Pants"; + if (equip_type == EQUIP_TUNIC) return "Skin Tunic"; + if (equip_type == EQUIP_MOCCASINS) return "Moccasins"; + if (equip_type == EQUIP_POUCH) return "Skin Pouch"; + if (equip_type == EQUIP_BACKPACK) return "Backpack"; + return "Unknown"; +} + +// Get inventory count for an equipment type +int get_unruned_equipment_count(int equip_type) { + if (equip_type == EQUIP_SPEAR) return inv_spears; + if (equip_type == EQUIP_AXE) return inv_axes; + if (equip_type == EQUIP_SLING) return inv_slings; + if (equip_type == EQUIP_BOW) return inv_bows; + if (equip_type == EQUIP_HAT) return inv_skin_hats; + if (equip_type == EQUIP_GLOVES) return inv_skin_gloves; + if (equip_type == EQUIP_PANTS) return inv_skin_pants; + if (equip_type == EQUIP_TUNIC) return inv_skin_tunics; + if (equip_type == EQUIP_MOCCASINS) return inv_moccasins; + if (equip_type == EQUIP_POUCH) return inv_skin_pouches; + if (equip_type == EQUIP_BACKPACK) return inv_backpacks; + return 0; +} + +// Decrement inventory for an equipment type +void decrement_unruned_equipment(int equip_type) { + if (equip_type == EQUIP_SPEAR) { inv_spears--; return; } + if (equip_type == EQUIP_AXE) { inv_axes--; return; } + if (equip_type == EQUIP_SLING) { inv_slings--; return; } + if (equip_type == EQUIP_BOW) { inv_bows--; return; } + if (equip_type == EQUIP_HAT) { inv_skin_hats--; return; } + if (equip_type == EQUIP_GLOVES) { inv_skin_gloves--; return; } + if (equip_type == EQUIP_PANTS) { inv_skin_pants--; return; } + if (equip_type == EQUIP_TUNIC) { inv_skin_tunics--; return; } + if (equip_type == EQUIP_MOCCASINS) { inv_moccasins--; return; } + if (equip_type == EQUIP_POUCH) { inv_skin_pouches--; return; } + if (equip_type == EQUIP_BACKPACK) { inv_backpacks--; return; } +} + +void run_runes_menu() { + // Check if in base area + if (x > BASE_END) { + speak_with_history("Rune engraving can only be done in the base area.", true); + return; + } + + speak_with_history("Runes.", true); + + // Build list of unlocked runes + string[] rune_options; + int[] rune_types; + + if (rune_swiftness_unlocked) { + rune_options.insert_last("Rune of Swiftness (1 Clay, 1 Favor) [Requires Knife]"); + rune_types.insert_last(RUNE_SWIFTNESS); + } + + if (rune_options.length() == 0) { + speak_with_history("No runes unlocked yet.", true); + return; + } + + int selection = 0; + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + speak_with_history("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= int(rune_options.length())) selection = 0; + speak_with_history(rune_options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = int(rune_options.length()) - 1; + speak_with_history(rune_options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + int rune_type = rune_types[selection]; + run_rune_equipment_menu(rune_type); + break; + } + } +} + +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; + + int[] runeable = get_runeable_equipment_types(); + for (uint i = 0; i < runeable.length(); i++) { + int equip_type = runeable[i]; + int unruned_count = get_unruned_equipment_count(equip_type); + if (unruned_count > 0) { + string name = get_base_equipment_name(equip_type); + equipment_options.insert_last(name + " (" + unruned_count + " available)"); + equipment_types.insert_last(equip_type); + } + } + + if (equipment_options.length() == 0) { + speak_with_history("No equipment available to engrave.", true); + return; + } + + int selection = 0; + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + speak_with_history("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= int(equipment_options.length())) selection = 0; + speak_with_history(equipment_options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = int(equipment_options.length()) - 1; + speak_with_history(equipment_options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + int equip_type = equipment_types[selection]; + engrave_rune(equip_type, rune_type); + break; + } + } +} + +void engrave_rune(int equip_type, int rune_type) { + // Validate requirements + string missing = ""; + if (inv_knives < 1) missing += "Stone Knife "; + if (inv_clay < 1) missing += "1 clay "; + if (favor < 1.0) missing += "1 favor "; + + // Check equipment is still available + if (get_unruned_equipment_count(equip_type) < 1) { + speak_with_history("No " + get_base_equipment_name(equip_type) + " available.", true); + return; + } + + if (missing == "") { + // Consume materials (knife is not consumed, it's a tool) + inv_clay--; + favor -= 1.0; + + // Remove one unruned item from inventory + decrement_unruned_equipment(equip_type); + + // Add one runed item to dictionary + add_runed_item(equip_type, rune_type); + + // Play crafting animation + simulate_crafting(6); + + string runed_name = "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); + speak_with_history("Engraved " + runed_name + ".", true); + } else { + speak_with_history("Missing: " + missing, true); + } +} diff --git a/src/crafting/crafting_core.nvgt b/src/crafting/crafting_core.nvgt index 4800caf..1348cd4 100644 --- a/src/crafting/crafting_core.nvgt +++ b/src/crafting/crafting_core.nvgt @@ -11,8 +11,28 @@ void run_crafting_menu() { speak_with_history("Crafting menu.", true); int selection = 0; - string[] categories = {"Weapons", "Tools", "Materials", "Clothing", "Buildings", "Barricade"}; - int[] category_types = {0, 1, 2, 3, 4, 5}; + string[] categories; + int[] category_types; + + // Build categories dynamically + categories.insert_last("Weapons"); + category_types.insert_last(0); + categories.insert_last("Tools"); + category_types.insert_last(1); + categories.insert_last("Materials"); + category_types.insert_last(2); + categories.insert_last("Clothing"); + category_types.insert_last(3); + categories.insert_last("Buildings"); + category_types.insert_last(4); + categories.insert_last("Barricade"); + category_types.insert_last(5); + + // Add Runes category if any rune is unlocked + if (any_rune_unlocked()) { + categories.insert_last("Runes"); + category_types.insert_last(6); + } while(true) { wait(5); @@ -24,13 +44,13 @@ void run_crafting_menu() { if (key_pressed(KEY_DOWN)) { selection++; - if (selection >= categories.length()) selection = 0; + if (selection >= int(categories.length())) selection = 0; speak_with_history(categories[selection], true); } if (key_pressed(KEY_UP)) { selection--; - if (selection < 0) selection = categories.length() - 1; + if (selection < 0) selection = int(categories.length()) - 1; speak_with_history(categories[selection], true); } @@ -42,6 +62,7 @@ void run_crafting_menu() { else if (category == 3) run_clothing_menu(); else if (category == 4) run_buildings_menu(); else if (category == 5) run_barricade_menu(); + else if (category == 6) run_runes_menu(); break; } } diff --git a/src/enemies/bandit.nvgt b/src/enemies/bandit.nvgt index a984ee7..0e0419b 100644 --- a/src/enemies/bandit.nvgt +++ b/src/enemies/bandit.nvgt @@ -181,6 +181,13 @@ void update_bandit(Bandit@ bandit) { if (bandit.alert_timer.elapsed > bandit.next_alert_delay) { bandit.alert_timer.restart(); bandit.next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY); + + // Destroy old sound handle before playing new one to prevent overlapping + if (bandit.sound_handle != -1) { + p.destroy_sound(bandit.sound_handle); + bandit.sound_handle = -1; + } + bandit.sound_handle = play_creature_voice(bandit.alert_sound, x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } diff --git a/src/enemies/flying_creatures.nvgt b/src/enemies/flying_creatures.nvgt index 3a4d0fb..da0ce12 100644 --- a/src/enemies/flying_creatures.nvgt +++ b/src/enemies/flying_creatures.nvgt @@ -330,7 +330,6 @@ void update_flying_creature(FlyingCreature@ creature) { } play_creature_death_sound(cfg.impact_sound, x, creature.position, cfg.sound_volume_step); - notify("A " + creature.creature_type + " fell from the sky at " + creature.position + "!"); add_world_drop(creature.position, cfg.drop_type); creature.health = 0; } diff --git a/src/enemies/undead.nvgt b/src/enemies/undead.nvgt index 4907392..f2f78e3 100644 --- a/src/enemies/undead.nvgt +++ b/src/enemies/undead.nvgt @@ -134,6 +134,13 @@ void update_undead(Undead@ undead) { if (undead.groan_timer.elapsed > undead.next_groan_delay) { undead.groan_timer.restart(); undead.next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY); + + // Destroy old sound handle before playing new one to prevent overlapping + if (undead.sound_handle != -1) { + p.destroy_sound(undead.sound_handle); + undead.sound_handle = -1; + } + undead.sound_handle = play_creature_voice(undead.voice_sound, x, undead.position, ZOMBIE_SOUND_VOLUME_STEP); } diff --git a/src/environment.nvgt b/src/environment.nvgt index 3edf697..c315844 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -86,6 +86,11 @@ class Tree { if (tree_distance <= TREE_SOUND_RANGE) { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { + // Clean up invalid handle if it exists + if (sound_handle != -1) { + p.destroy_sound(sound_handle); + sound_handle = -1; + } sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true); if (sound_handle != -1) { p.update_sound_positioning_values(sound_handle, -1.0, TREE_SOUND_VOLUME_STEP, true); @@ -923,6 +928,9 @@ void start_rope_climb(bool climbing_up, int target_x, int target_elevation) { string direction = rope_climb_up ? "up" : "down"; speak_with_history("Climbing " + direction + ". " + distance + " feet.", true); + + // Start looping rope climbing sound + rope_climb_sound_handle = p.play_stationary("sounds/actions/climb_rope.ogg", true); } void update_rope_climbing() { @@ -943,11 +951,9 @@ void update_rope_climbing() { if (y < rope_climb_target_y) { y++; - // Check if we've reached the target BEFORE playing the sound + // Check if we've reached the target if (y >= rope_climb_target_y) { complete_rope_climb(); - } else { - p.play_stationary("sounds/actions/climb_rope.ogg", false); } } } else { @@ -955,11 +961,9 @@ void update_rope_climbing() { if (y > rope_climb_target_y) { y--; - // Check if we've reached the target BEFORE playing the sound + // Check if we've reached the target if (y <= rope_climb_target_y) { complete_rope_climb(); - } else { - p.play_stationary("sounds/actions/climb_rope.ogg", false); } } } @@ -971,6 +975,12 @@ void complete_rope_climb() { x = rope_climb_target_x; y = rope_climb_target_y; + // Stop looping rope climbing sound + if (rope_climb_sound_handle != -1) { + p.destroy_sound(rope_climb_sound_handle); + rope_climb_sound_handle = -1; + } + // Play footstep for new terrain play_footstep(x, BASE_END, GRASS_END); @@ -981,6 +991,19 @@ void check_rope_climb_fall() { if (!rope_climbing) return; if (key_down(KEY_LEFT) || key_down(KEY_RIGHT)) { + // Stop rope climbing sound + if (rope_climb_sound_handle != -1) { + p.destroy_sound(rope_climb_sound_handle); + rope_climb_sound_handle = -1; + } + + // Move one tile in the direction being pressed before falling + if (key_down(KEY_LEFT) && x > 0) { + x--; + } else if (key_down(KEY_RIGHT) && x < MAP_SIZE - 1) { + x++; + } + // Fall from rope! rope_climbing = false; start_falling(); diff --git a/src/inventory.nvgt b/src/inventory.nvgt index 625756e..797dc5a 100644 --- a/src/inventory.nvgt +++ b/src/inventory.nvgt @@ -1,4 +1,6 @@ // Inventory module includes #include "src/inventory_items.nvgt" +#include "src/runes/rune_data.nvgt" +#include "src/runes/rune_effects.nvgt" #include "src/inventory_menus.nvgt" #include "src/crafting.nvgt" diff --git a/src/inventory_items.nvgt b/src/inventory_items.nvgt index 7b004f8..d3b05d9 100644 --- a/src/inventory_items.nvgt +++ b/src/inventory_items.nvgt @@ -261,11 +261,21 @@ void update_max_health_from_equipment() { if (player_health > max_health) { player_health = max_health; } + + // Calculate walk speed with all bonuses int base_speed = (equipped_feet == EQUIP_MOCCASINS) ? MOCCASINS_WALK_SPEED : BASE_WALK_SPEED; - walk_speed = base_speed; + + // Apply rune speed bonuses (stacks with moccasins) + int rune_bonus = get_total_rune_walk_speed_bonus(); + walk_speed = base_speed - rune_bonus; + + // Apply blessing (if active and faster than current) if (blessing_speed_active && BLESSING_WALK_SPEED < walk_speed) { walk_speed = BLESSING_WALK_SPEED; } + + // Ensure minimum walk speed + if (walk_speed < 200) walk_speed = 200; } int get_quick_slot_key() { @@ -522,9 +532,21 @@ string get_equipped_weapon_name() { } string get_speed_status() { - if (blessing_speed_active) return "blessed"; - if (equipped_feet == EQUIP_MOCCASINS) return "boosted by moccasins"; - return "normal"; + string status = ""; + int rune_bonus = get_total_rune_walk_speed_bonus(); + + if (blessing_speed_active) { + status = "blessed"; + } else if (equipped_feet == EQUIP_MOCCASINS && rune_bonus > 0) { + status = "boosted by moccasins and runes"; + } else if (equipped_feet == EQUIP_MOCCASINS) { + status = "boosted by moccasins"; + } else if (rune_bonus > 0) { + status = "boosted by runes"; + } else { + status = "normal"; + } + return status; } void cleanup_equipment_after_inventory_change() { diff --git a/src/menus/action_menu.nvgt b/src/menus/action_menu.nvgt index 7333146..1d23015 100644 --- a/src/menus/action_menu.nvgt +++ b/src/menus/action_menu.nvgt @@ -9,6 +9,12 @@ void check_action_menu(int x) { void try_place_snare(int x) { if (inv_snares > 0) { + // Prevent placing in base area + if (x <= BASE_END) { + speak_with_history("Cannot place snares in the base area.", true); + return; + } + // Prevent placing if one already exists here if (get_snare_at(x) != null) { speak_with_history("There is already a snare here.", true); diff --git a/src/menus/equipment_menu.nvgt b/src/menus/equipment_menu.nvgt index decab5c..c09c697 100644 --- a/src/menus/equipment_menu.nvgt +++ b/src/menus/equipment_menu.nvgt @@ -1,13 +1,30 @@ // Equipment menu system // Functions for managing equipment (weapons and clothing) +// Check if player has any equipment (including runed items) +bool has_any_equipment() { + // Check unruned items + if (inv_spears > 0 || inv_axes > 0 || inv_slings > 0 || inv_bows > 0 || + inv_skin_hats > 0 || inv_skin_gloves > 0 || inv_skin_pants > 0 || + inv_skin_tunics > 0 || inv_moccasins > 0 || inv_skin_pouches > 0 || + inv_backpacks > 0) { + return true; + } + + // Check runed items + int[] runeable = get_runeable_equipment_types(); + for (uint i = 0; i < runeable.length(); i++) { + if (get_runed_item_count(runeable[i], RUNE_SWIFTNESS) > 0) { + return true; + } + } + + return false; +} + void check_equipment_menu() { if (key_pressed(KEY_E)) { - // Check if player has any equipment - if (inv_spears == 0 && inv_axes == 0 && inv_slings == 0 && - inv_skin_hats == 0 && inv_skin_gloves == 0 && inv_skin_pants == 0 && - inv_skin_tunics == 0 && inv_moccasins == 0 && inv_skin_pouches == 0 && - inv_backpacks == 0) { + if (!has_any_equipment()) { speak_with_history("Nothing to equip.", true); } else { run_equipment_menu(); @@ -15,63 +32,109 @@ void check_equipment_menu() { } } +// Helper to get full equipment name including rune +string get_full_equipment_name(int equip_type, int rune_type) { + string base_name = get_base_equipment_name(equip_type); + if (rune_type != RUNE_NONE) { + return "Runed " + base_name + " of " + get_rune_effect_name(rune_type); + } + return base_name; +} + +// Check if an item with specific rune is equipped +bool is_runed_item_equipped(int equip_type, int rune_type) { + if (!equipment_is_equipped(equip_type)) return false; + return get_equipped_rune_for_slot(equip_type) == rune_type; +} + void run_equipment_menu() { speak_with_history("Equipment menu.", true); int selection = 0; string[] options; int[] equipment_types; + int[] rune_types; // Track which rune is on each option (RUNE_NONE for unruned) - // Build menu dynamically based on what player has + // Add unruned items if (inv_spears > 0) { - string status = equipment_is_equipped(EQUIP_SPEAR) ? " (equipped)" : ""; + string status = is_runed_item_equipped(EQUIP_SPEAR, RUNE_NONE) ? " (equipped)" : ""; options.insert_last("Spear" + status); equipment_types.insert_last(EQUIP_SPEAR); + rune_types.insert_last(RUNE_NONE); } if (inv_slings > 0) { - string status = equipment_is_equipped(EQUIP_SLING) ? " (equipped)" : ""; + string status = is_runed_item_equipped(EQUIP_SLING, RUNE_NONE) ? " (equipped)" : ""; options.insert_last("Sling" + status); equipment_types.insert_last(EQUIP_SLING); + rune_types.insert_last(RUNE_NONE); } if (inv_axes > 0) { - string status = equipment_is_equipped(EQUIP_AXE) ? " (equipped)" : ""; + string status = is_runed_item_equipped(EQUIP_AXE, RUNE_NONE) ? " (equipped)" : ""; options.insert_last("Stone Axe" + status); equipment_types.insert_last(EQUIP_AXE); + rune_types.insert_last(RUNE_NONE); + } + if (inv_bows > 0) { + string status = is_runed_item_equipped(EQUIP_BOW, RUNE_NONE) ? " (equipped)" : ""; + options.insert_last("Bow" + status); + equipment_types.insert_last(EQUIP_BOW); + rune_types.insert_last(RUNE_NONE); } if (inv_skin_hats > 0) { - string status = equipment_is_equipped(EQUIP_HAT) ? " (equipped)" : ""; + string status = is_runed_item_equipped(EQUIP_HAT, RUNE_NONE) ? " (equipped)" : ""; options.insert_last("Skin Hat" + status); equipment_types.insert_last(EQUIP_HAT); + rune_types.insert_last(RUNE_NONE); } if (inv_skin_gloves > 0) { - string status = equipment_is_equipped(EQUIP_GLOVES) ? " (equipped)" : ""; + string status = is_runed_item_equipped(EQUIP_GLOVES, RUNE_NONE) ? " (equipped)" : ""; options.insert_last("Skin Gloves" + status); equipment_types.insert_last(EQUIP_GLOVES); + rune_types.insert_last(RUNE_NONE); } if (inv_skin_pants > 0) { - string status = equipment_is_equipped(EQUIP_PANTS) ? " (equipped)" : ""; + string status = is_runed_item_equipped(EQUIP_PANTS, RUNE_NONE) ? " (equipped)" : ""; options.insert_last("Skin Pants" + status); equipment_types.insert_last(EQUIP_PANTS); + rune_types.insert_last(RUNE_NONE); } if (inv_skin_tunics > 0) { - string status = equipment_is_equipped(EQUIP_TUNIC) ? " (equipped)" : ""; + string status = is_runed_item_equipped(EQUIP_TUNIC, RUNE_NONE) ? " (equipped)" : ""; options.insert_last("Skin Tunic" + status); equipment_types.insert_last(EQUIP_TUNIC); + rune_types.insert_last(RUNE_NONE); } if (inv_moccasins > 0) { - string status = equipment_is_equipped(EQUIP_MOCCASINS) ? " (equipped)" : ""; + string status = is_runed_item_equipped(EQUIP_MOCCASINS, RUNE_NONE) ? " (equipped)" : ""; options.insert_last("Moccasins" + status); equipment_types.insert_last(EQUIP_MOCCASINS); + rune_types.insert_last(RUNE_NONE); } if (inv_skin_pouches > 0) { - string status = equipment_is_equipped(EQUIP_POUCH) ? " (equipped)" : ""; + string status = is_runed_item_equipped(EQUIP_POUCH, RUNE_NONE) ? " (equipped)" : ""; options.insert_last("Skin Pouch" + status); equipment_types.insert_last(EQUIP_POUCH); + rune_types.insert_last(RUNE_NONE); } if (inv_backpacks > 0) { - string status = equipment_is_equipped(EQUIP_BACKPACK) ? " (equipped)" : ""; + string status = is_runed_item_equipped(EQUIP_BACKPACK, RUNE_NONE) ? " (equipped)" : ""; options.insert_last("Backpack" + status); equipment_types.insert_last(EQUIP_BACKPACK); + rune_types.insert_last(RUNE_NONE); + } + + // Add runed items (currently only swiftness rune) + int[] runeable = get_runeable_equipment_types(); + for (uint i = 0; i < runeable.length(); i++) { + int equip_type = runeable[i]; + int count = get_runed_item_count(equip_type, RUNE_SWIFTNESS); + if (count > 0) { + string name = get_full_equipment_name(equip_type, RUNE_SWIFTNESS); + string status = is_runed_item_equipped(equip_type, RUNE_SWIFTNESS) ? " (equipped)" : ""; + options.insert_last(name + status); + equipment_types.insert_last(equip_type); + rune_types.insert_last(RUNE_SWIFTNESS); + } } while(true) { @@ -84,31 +147,40 @@ void run_equipment_menu() { if (key_pressed(KEY_DOWN)) { selection++; - if (selection >= options.length()) selection = 0; + if (selection >= int(options.length())) selection = 0; speak_with_history(options[selection], true); } if (key_pressed(KEY_UP)) { selection--; - if (selection < 0) selection = options.length() - 1; + if (selection < 0) selection = int(options.length()) - 1; speak_with_history(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]; quick_slots[slot_index] = equip_type; - speak_with_history(get_equipment_name(equip_type) + " set to slot " + slot_index + ".", true); + 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]; - if (equipment_is_equipped(equip_type)) { + int rune_type = rune_types[selection]; + string name = get_full_equipment_name(equip_type, rune_type); + + if (is_runed_item_equipped(equip_type, rune_type)) { + // Unequip unequip_equipment_type(equip_type); - speak_with_history(get_equipment_name(equip_type) + " unequipped.", true); + clear_equipped_rune_for_slot(equip_type); + speak_with_history(name + " unequipped.", true); } else { + // Equip equip_equipment_type(equip_type); - speak_with_history(get_equipment_name(equip_type) + " equipped.", true); + set_equipped_rune_for_slot(equip_type, rune_type); + speak_with_history(name + " equipped.", true); } update_max_health_from_equipment(); break; diff --git a/src/player.nvgt b/src/player.nvgt index ba93a3e..97d4b83 100644 --- a/src/player.nvgt +++ b/src/player.nvgt @@ -17,6 +17,7 @@ bool rope_climb_up = true; int rope_climb_target_x = 0; int rope_climb_target_y = 0; int rope_climb_start_y = 0; +int rope_climb_sound_handle = -1; timer rope_climb_timer; int pending_rope_climb_x = -1; int pending_rope_climb_elevation = 0; diff --git a/src/runes/rune_data.nvgt b/src/runes/rune_data.nvgt new file mode 100644 index 0000000..03f34b8 --- /dev/null +++ b/src/runes/rune_data.nvgt @@ -0,0 +1,158 @@ +// Rune System - Core Data and Helper Functions +// Runes can be engraved onto equipment to grant permanent bonuses + +// Rune type constants +const int RUNE_NONE = -1; +const int RUNE_SWIFTNESS = 0; + +// Rune unlock tracking (persisted in save) +bool rune_swiftness_unlocked = false; + +// Boss defeat tracking +bool unicorn_boss_defeated = false; + +// Speed bonus per rune of swiftness (milliseconds) - half of moccasins (40ms) +const int RUNE_SWIFTNESS_SPEED_BONUS = 20; + +// Gathering speed reduction per rune (percentage off base time) +const int RUNE_SWIFTNESS_GATHER_BONUS = 5; + +// Dictionary for runed item counts +// Key format: "equip_type:rune_type" (e.g., "5:0" for pants with swiftness) +// Value: count of that runed item type +dictionary runed_items; + +// Track which rune is on equipped items +int equipped_head_rune = RUNE_NONE; +int equipped_torso_rune = RUNE_NONE; +int equipped_arms_rune = RUNE_NONE; +int equipped_hands_rune = RUNE_NONE; +int equipped_legs_rune = RUNE_NONE; +int equipped_feet_rune = RUNE_NONE; +int equipped_weapon_rune = RUNE_NONE; + +// Get display name for a rune type +string get_rune_name(int rune_type) { + if (rune_type == RUNE_SWIFTNESS) return "Rune of Swiftness"; + return "Unknown Rune"; +} + +// Get the effect suffix for runed item names (e.g., "of Quickness") +string get_rune_effect_name(int rune_type) { + if (rune_type == RUNE_SWIFTNESS) return "Quickness"; + return "Unknown"; +} + +// Check if any rune has been unlocked +bool any_rune_unlocked() { + return rune_swiftness_unlocked; +} + +// Check if a specific rune is unlocked +bool is_rune_unlocked(int rune_type) { + if (rune_type == RUNE_SWIFTNESS) return rune_swiftness_unlocked; + return false; +} + +// Create dictionary key for runed item storage +string make_runed_key(int equip_type, int rune_type) { + return "" + equip_type + ":" + rune_type; +} + +// Get count of a specific runed item type +int get_runed_item_count(int equip_type, int rune_type) { + string key = make_runed_key(equip_type, rune_type); + if (runed_items.exists(key)) { + return int(runed_items[key]); + } + return 0; +} + +// Add a runed item to inventory +void add_runed_item(int equip_type, int rune_type) { + string key = make_runed_key(equip_type, rune_type); + int current = get_runed_item_count(equip_type, rune_type); + runed_items.set(key, current + 1); +} + +// Remove a runed item from inventory +void remove_runed_item(int equip_type, int rune_type) { + string key = make_runed_key(equip_type, rune_type); + int current = get_runed_item_count(equip_type, rune_type); + if (current > 0) { + runed_items.set(key, current - 1); + } +} + +// Check if player has any runed version of an equipment type +bool has_any_runed_version(int equip_type) { + // Check all rune types + if (get_runed_item_count(equip_type, RUNE_SWIFTNESS) > 0) return true; + return false; +} + +// Get the rune type on equipped item for a given slot +int get_equipped_rune_for_slot(int equip_type) { + if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || + equip_type == EQUIP_SLING || equip_type == EQUIP_BOW) { + return equipped_weapon_rune; + } + if (equip_type == EQUIP_HAT) return equipped_head_rune; + if (equip_type == EQUIP_TUNIC) return equipped_torso_rune; + if (equip_type == EQUIP_POUCH || equip_type == EQUIP_BACKPACK) return equipped_arms_rune; + if (equip_type == EQUIP_GLOVES) return equipped_hands_rune; + if (equip_type == EQUIP_PANTS) return equipped_legs_rune; + if (equip_type == EQUIP_MOCCASINS) return equipped_feet_rune; + return RUNE_NONE; +} + +// Set the rune on an equipped item slot +void set_equipped_rune_for_slot(int equip_type, int rune_type) { + if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || + equip_type == EQUIP_SLING || equip_type == EQUIP_BOW) { + equipped_weapon_rune = rune_type; + return; + } + if (equip_type == EQUIP_HAT) { equipped_head_rune = rune_type; return; } + if (equip_type == EQUIP_TUNIC) { equipped_torso_rune = rune_type; return; } + if (equip_type == EQUIP_POUCH || equip_type == EQUIP_BACKPACK) { equipped_arms_rune = rune_type; return; } + if (equip_type == EQUIP_GLOVES) { equipped_hands_rune = rune_type; return; } + if (equip_type == EQUIP_PANTS) { equipped_legs_rune = rune_type; return; } + if (equip_type == EQUIP_MOCCASINS) { equipped_feet_rune = rune_type; return; } +} + +// Clear rune from an equipped slot +void clear_equipped_rune_for_slot(int equip_type) { + set_equipped_rune_for_slot(equip_type, RUNE_NONE); +} + +// Get all equipment types that can be runed +int[] get_runeable_equipment_types() { + int[] types; + types.insert_last(EQUIP_SPEAR); + types.insert_last(EQUIP_AXE); + types.insert_last(EQUIP_SLING); + types.insert_last(EQUIP_BOW); + types.insert_last(EQUIP_HAT); + types.insert_last(EQUIP_GLOVES); + types.insert_last(EQUIP_PANTS); + types.insert_last(EQUIP_TUNIC); + types.insert_last(EQUIP_MOCCASINS); + types.insert_last(EQUIP_POUCH); + types.insert_last(EQUIP_BACKPACK); + return types; +} + +// Reset all rune data for new game +void reset_rune_data() { + rune_swiftness_unlocked = false; + unicorn_boss_defeated = false; + runed_items.delete_all(); + equipped_head_rune = RUNE_NONE; + equipped_torso_rune = RUNE_NONE; + equipped_arms_rune = RUNE_NONE; + equipped_hands_rune = RUNE_NONE; + equipped_legs_rune = RUNE_NONE; + equipped_feet_rune = RUNE_NONE; + equipped_weapon_rune = RUNE_NONE; +} diff --git a/src/runes/rune_effects.nvgt b/src/runes/rune_effects.nvgt new file mode 100644 index 0000000..4005d7f --- /dev/null +++ b/src/runes/rune_effects.nvgt @@ -0,0 +1,81 @@ +// Rune Effects - Calculate bonuses from equipped runed items +// This file handles the actual gameplay effects of runes + +// Calculate total walking speed bonus from all equipped runed items +int get_total_rune_walk_speed_bonus() { + int bonus = 0; + + // Check each equipment slot for swiftness runes + if (equipped_head_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_torso_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_arms_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_hands_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_legs_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_feet_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_weapon_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; + + return bonus; +} + +// Calculate gathering speed reduction percentage from equipped runed items +// Returns total percentage reduction (e.g., 15 means 15% faster gathering) +int get_total_rune_gather_bonus() { + int bonus = 0; + + // Check each equipment slot for swiftness runes + if (equipped_head_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_torso_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_arms_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_hands_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_legs_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_feet_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_weapon_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; + + return bonus; +} + +// Apply gathering time reduction based on rune bonuses +// Takes base time in ms, returns reduced time +int apply_rune_gather_bonus(int base_time) { + int bonus_percent = get_total_rune_gather_bonus(); + if (bonus_percent <= 0) return base_time; + + // Cap at 50% reduction to prevent instant gathering + if (bonus_percent > 50) bonus_percent = 50; + + int reduction = (base_time * bonus_percent) / 100; + return base_time - reduction; +} + +// Count total equipped swiftness runes +int count_equipped_swiftness_runes() { + int count = 0; + if (equipped_head_rune == RUNE_SWIFTNESS) count++; + if (equipped_torso_rune == RUNE_SWIFTNESS) count++; + if (equipped_arms_rune == RUNE_SWIFTNESS) count++; + if (equipped_hands_rune == RUNE_SWIFTNESS) count++; + if (equipped_legs_rune == RUNE_SWIFTNESS) count++; + if (equipped_feet_rune == RUNE_SWIFTNESS) count++; + if (equipped_weapon_rune == RUNE_SWIFTNESS) count++; + return count; +} + +// Get a human-readable description of current rune speed bonuses +string get_rune_speed_description() { + int walk_bonus = get_total_rune_walk_speed_bonus(); + int gather_bonus = get_total_rune_gather_bonus(); + + if (walk_bonus == 0 && gather_bonus == 0) { + return "none"; + } + + string desc = ""; + if (walk_bonus > 0) { + desc += walk_bonus + "ms faster walking"; + } + if (gather_bonus > 0) { + if (desc.length() > 0) desc += ", "; + desc += gather_bonus + "% faster gathering"; + } + return desc; +} diff --git a/src/save_system.nvgt b/src/save_system.nvgt index bed46cb..2d4b348 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -169,6 +169,7 @@ void reset_game_state() { rope_climb_target_x = 0; rope_climb_target_y = 0; rope_climb_start_y = 0; + rope_climb_sound_handle = -1; pending_rope_climb_x = -1; pending_rope_climb_elevation = 0; @@ -618,6 +619,29 @@ bool save_game_state() { } saveData.set("equipment_quick_slots", join_string_array(quickSlotData)); + // Rune system data + saveData.set("rune_swiftness_unlocked", rune_swiftness_unlocked); + saveData.set("unicorn_boss_defeated", unicorn_boss_defeated); + saveData.set("equipped_head_rune", equipped_head_rune); + saveData.set("equipped_torso_rune", equipped_torso_rune); + saveData.set("equipped_arms_rune", equipped_arms_rune); + saveData.set("equipped_hands_rune", equipped_hands_rune); + saveData.set("equipped_legs_rune", equipped_legs_rune); + saveData.set("equipped_feet_rune", equipped_feet_rune); + saveData.set("equipped_weapon_rune", equipped_weapon_rune); + + // Save runed items dictionary as key:value pairs + string[] runed_items_data; + string[]@ keys = runed_items.get_keys(); + for (uint i = 0; i < keys.length(); i++) { + string key = keys[i]; + int count = int(runed_items[key]); + if (count > 0) { + runed_items_data.insert_last(key + "=" + count); + } + } + saveData.set("runed_items", join_string_array(runed_items_data)); + saveData.set("time_current_hour", current_hour); saveData.set("time_current_day", current_day); saveData.set("time_is_daytime", is_daytime); @@ -919,6 +943,32 @@ bool load_game_state() { } update_max_health_from_equipment(); + // Load rune system data + rune_swiftness_unlocked = get_bool(saveData, "rune_swiftness_unlocked", false); + unicorn_boss_defeated = get_bool(saveData, "unicorn_boss_defeated", false); + equipped_head_rune = int(get_number(saveData, "equipped_head_rune", RUNE_NONE)); + equipped_torso_rune = int(get_number(saveData, "equipped_torso_rune", RUNE_NONE)); + equipped_arms_rune = int(get_number(saveData, "equipped_arms_rune", RUNE_NONE)); + equipped_hands_rune = int(get_number(saveData, "equipped_hands_rune", RUNE_NONE)); + equipped_legs_rune = int(get_number(saveData, "equipped_legs_rune", RUNE_NONE)); + equipped_feet_rune = int(get_number(saveData, "equipped_feet_rune", RUNE_NONE)); + equipped_weapon_rune = int(get_number(saveData, "equipped_weapon_rune", RUNE_NONE)); + + // Load runed items dictionary + runed_items.delete_all(); + string[] loaded_runed_items = get_string_list_or_split(saveData, "runed_items"); + for (uint i = 0; i < loaded_runed_items.length(); i++) { + string entry = loaded_runed_items[i]; + int eq_pos = entry.find("="); + if (eq_pos > 0) { + string key = entry.substr(0, eq_pos); + int count = parse_int(entry.substr(eq_pos + 1)); + if (count > 0) { + runed_items.set(key, count); + } + } + } + current_hour = int(get_number(saveData, "time_current_hour", 8)); current_day = int(get_number(saveData, "time_current_day", 1)); sun_setting_warned = get_bool(saveData, "time_sun_setting_warned", false); diff --git a/src/time_system.nvgt b/src/time_system.nvgt index aa36855..ca218db 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -99,14 +99,6 @@ void expand_regular_area() { int stream_start = random(0, EXPANSION_SIZE - stream_width); int actual_start = new_start + stream_start; add_world_stream(actual_start, stream_width); - - string width_desc = "very small"; - if (stream_width == 2) width_desc = "small"; - else if (stream_width == 3) width_desc = "medium"; - else if (stream_width == 4) width_desc = "wide"; - else if (stream_width == 5) width_desc = "very wide"; - - notify("A " + width_desc + " stream flows through the new area at x " + actual_start + "."); } else { // Try to place a tree with proper spacing and per-area limits spawn_tree_in_area(new_start, new_end); @@ -480,12 +472,22 @@ void start_crossfade(bool to_night) { if (to_night) { // Starting night sound if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) { + // Clean up invalid handle if it exists + if (night_sound_handle != -1) { + p.destroy_sound(night_sound_handle); + night_sound_handle = -1; + } night_sound_handle = p.play_stationary("sounds/nature/night.ogg", true); } p.update_sound_start_values(night_sound_handle, 0.0, CROSSFADE_MIN_VOLUME, 1.0); } else { // Starting day sound if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) { + // Clean up invalid handle if it exists + if (day_sound_handle != -1) { + p.destroy_sound(day_sound_handle); + day_sound_handle = -1; + } day_sound_handle = p.play_stationary("sounds/nature/day.ogg", true); } p.update_sound_start_values(day_sound_handle, 0.0, CROSSFADE_MIN_VOLUME, 1.0); @@ -550,6 +552,11 @@ void update_ambience(bool force_restart) { night_sound_handle = -1; } if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) { + // Clean up invalid handle if it exists + if (day_sound_handle != -1) { + p.destroy_sound(day_sound_handle); + day_sound_handle = -1; + } day_sound_handle = p.play_stationary("sounds/nature/day.ogg", true); } } else { @@ -559,6 +566,11 @@ void update_ambience(bool force_restart) { day_sound_handle = -1; } if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) { + // Clean up invalid handle if it exists + if (night_sound_handle != -1) { + p.destroy_sound(night_sound_handle); + night_sound_handle = -1; + } night_sound_handle = p.play_stationary("sounds/nature/night.ogg", true); } } diff --git a/src/weather.nvgt b/src/weather.nvgt index 608d070..8a95093 100644 --- a/src/weather.nvgt +++ b/src/weather.nvgt @@ -280,6 +280,12 @@ void update_wind_gusts() { void play_wind_gust() { if (wind_intensity == INTENSITY_NONE || wind_intensity > INTENSITY_HIGH) return; + // Clean up previous wind gust if it's stale + if (wind_sound_handle != -1 && !p.sound_is_active(wind_sound_handle)) { + p.destroy_sound(wind_sound_handle); + wind_sound_handle = -1; + } + // Play the appropriate wind sound once (non-looping) wind_sound_handle = p.play_stationary(wind_sounds[wind_intensity], false); } @@ -308,10 +314,17 @@ void fade_rain_to_intensity(int new_intensity) { rain_fade_duration = random(10000, 20000); // Start rain sound if not playing - if (rain_sound_handle == -1 && new_intensity != INTENSITY_NONE) { - rain_sound_handle = p.play_stationary(RAIN_SOUND, true); - p.update_sound_start_values(rain_sound_handle, 0.0, WEATHER_MIN_VOLUME, 1.0); - rain_fade_from_volume = WEATHER_MIN_VOLUME; + if (new_intensity != INTENSITY_NONE) { + if (rain_sound_handle == -1 || !p.sound_is_active(rain_sound_handle)) { + // Clean up invalid handle if it exists + if (rain_sound_handle != -1) { + p.destroy_sound(rain_sound_handle); + rain_sound_handle = -1; + } + rain_sound_handle = p.play_stationary(RAIN_SOUND, true); + p.update_sound_start_values(rain_sound_handle, 0.0, WEATHER_MIN_VOLUME, 1.0); + rain_fade_from_volume = WEATHER_MIN_VOLUME; + } } rain_fading = true; diff --git a/src/world/mountains.nvgt b/src/world/mountains.nvgt index d242914..12e82a0 100644 --- a/src/world/mountains.nvgt +++ b/src/world/mountains.nvgt @@ -42,6 +42,9 @@ class MountainRange { // Ensure at least one steep climb (≥7 feet) reaches into 10-20 elevation range ensure_steep_climb(); + // Enforce plateau spacing: at least 3 walkable tiles between steep sections + enforce_plateau_spacing(); + // Assign terrain types based on elevation for (int i = 0; i < size; i++) { if (elevations[i] > 20) { @@ -94,6 +97,77 @@ class MountainRange { } } + void enforce_plateau_spacing() { + int size = int(elevations.length()); + const int MIN_PLATEAU_TILES = 3; + const int MAX_GENTLE_SLOPE = 6; // Max slope per tile that doesn't require rope + + // Find all steep sections (positions where rope would be needed) + int[] steep_positions; + for (int i = 1; i < size; i++) { + int diff = elevations[i] - elevations[i-1]; + if (diff < 0) diff = -diff; + if (diff >= MOUNTAIN_STEEP_THRESHOLD) { + steep_positions.insert_last(i); + } + } + + // For each steep section, ensure MIN_PLATEAU_TILES of gentle slope follow it + for (uint s = 0; s < steep_positions.length(); s++) { + int steep_pos = steep_positions[s]; + + // Check if we have enough tiles after this steep section for a plateau + if (steep_pos + MIN_PLATEAU_TILES >= size) continue; + + // Get the elevation after the steep climb + int plateau_start_elevation = elevations[steep_pos]; + + // Ensure next MIN_PLATEAU_TILES have gentle slopes + for (int p = 1; p <= MIN_PLATEAU_TILES; p++) { + int tile_idx = steep_pos + p; + if (tile_idx >= size) break; + + // Calculate max allowed elevation change for gentle slope + int prev_elevation = elevations[tile_idx - 1]; + int max_up = prev_elevation + MAX_GENTLE_SLOPE; + int max_down = prev_elevation - MAX_GENTLE_SLOPE; + + // Clamp to gentle slope range + if (elevations[tile_idx] > max_up) { + elevations[tile_idx] = max_up; + } else if (elevations[tile_idx] < max_down) { + elevations[tile_idx] = max_down; + } + + // Never go below 0 + if (elevations[tile_idx] < 0) { + elevations[tile_idx] = 0; + } + } + } + + // Final pass: ensure no negative elevations anywhere + for (int i = 0; i < size; i++) { + if (elevations[i] < 0) elevations[i] = 0; + } + + // Prefer climbing up before descending by adjusting early terrain + // If first half tends downward, flip it to go up first + if (size >= 10) { + int mid = size / 2; + int early_trend = elevations[mid] - elevations[0]; + + if (early_trend < 0) { + // Terrain goes down in first half, flip to go up + for (int i = 0; i <= mid; i++) { + elevations[i] = elevations[0] + (elevations[mid] - elevations[0]) * i / mid; + } + // Re-smooth after adjustment + smooth_slopes(); + } + } + } + void place_streams() { // Find valley bottoms (local minima) int[] valleys; @@ -247,6 +321,11 @@ class MountainRange { // Keep nearest stream sound active so distance-based fade can work. if (nearest_stream != -1) { if (stream_sound_handle == -1 || !p.sound_is_active(stream_sound_handle)) { + // Clean up invalid handle if it exists + if (stream_sound_handle != -1) { + p.destroy_sound(stream_sound_handle); + stream_sound_handle = -1; + } stream_sound_handle = p.play_1d("sounds/terrain/stream.ogg", x, nearest_stream, true); stream_sound_position = nearest_stream; if (stream_sound_handle != -1) { diff --git a/src/world/world_drops.nvgt b/src/world/world_drops.nvgt index 16c6437..50288c7 100644 --- a/src/world/world_drops.nvgt +++ b/src/world/world_drops.nvgt @@ -19,6 +19,11 @@ class WorldDrop { void update() { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { + // Clean up invalid handle if it exists + if (sound_handle != -1) { + p.destroy_sound(sound_handle); + sound_handle = -1; + } sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true); if (sound_handle != -1) { p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true); diff --git a/src/world/world_fires.nvgt b/src/world/world_fires.nvgt index 6ad3040..c2e26ca 100644 --- a/src/world/world_fires.nvgt +++ b/src/world/world_fires.nvgt @@ -56,6 +56,11 @@ class WorldFire { if (fire_distance < 0) fire_distance = -fire_distance; if (fire_distance <= FIRE_SOUND_RANGE) { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { + // Clean up invalid handle if it exists + if (sound_handle != -1) { + p.destroy_sound(sound_handle); + sound_handle = -1; + } sound_handle = p.play_1d("sounds/items/fire.ogg", x, position, true); if (sound_handle != -1) { p.update_sound_positioning_values(sound_handle, -1.0, FIRE_SOUND_VOLUME_STEP, true); diff --git a/src/world/world_snares.nvgt b/src/world/world_snares.nvgt index 27218a5..afac7b9 100644 --- a/src/world/world_snares.nvgt +++ b/src/world/world_snares.nvgt @@ -43,6 +43,11 @@ class WorldSnare { if (snare_distance <= SNARE_SOUND_RANGE) { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { + // Clean up invalid handle if it exists + if (sound_handle != -1) { + p.destroy_sound(sound_handle); + sound_handle = -1; + } sound_handle = p.play_1d("sounds/actions/set_snare.ogg", x, position, true); if (sound_handle != -1) { p.update_sound_positioning_values(sound_handle, SNARE_SOUND_PAN_STEP, SNARE_SOUND_VOLUME_STEP, true); diff --git a/src/world/world_streams.nvgt b/src/world/world_streams.nvgt index c9639ed..9e7af01 100644 --- a/src/world/world_streams.nvgt +++ b/src/world/world_streams.nvgt @@ -37,7 +37,14 @@ class WorldStream { } // Keep stream sound active so distance-based fade can work. - if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { + // Check if sound needs to be created or recreated + bool need_sound = (sound_handle == -1 || !p.sound_is_active(sound_handle)); + if (need_sound) { + // Clean up invalid handle if it exists + if (sound_handle != -1) { + p.destroy_sound(sound_handle); + sound_handle = -1; + } sound_handle = p.play_1d("sounds/terrain/stream.ogg", x, sound_pos, true); sound_position = sound_pos; if (sound_handle != -1) {