diff --git a/draugnorak.nvgt b/draugnorak.nvgt index a14df01..d601e00 100644 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -34,6 +34,7 @@ sound_pool p(100); #include "src/creature_audio.nvgt" #include "src/notify.nvgt" #include "src/speech_history.nvgt" +#include "src/bosses/adventure_system.nvgt" int run_main_menu() { speak_with_history("Draugnorak. Main menu.", true); @@ -192,6 +193,7 @@ void main() // Inventory & Actions check_inventory_keys(x); check_action_menu(x); + check_adventure_menu(x); check_crafting_menu(x, BASE_END); check_altar_menu(x); check_equipment_menu(); @@ -209,7 +211,11 @@ void main() // Coordinates Key if (key_pressed(KEY_X)) { string direction_label = (facing == 1) ? "east" : "west"; - speak_with_history(direction_label + ", x " + x + ", y " + y, true); + string terrain = get_terrain_at_position(x); + if (get_mountain_at(x) !is null) { + terrain += ". Mountains"; + } + speak_with_history(direction_label + ", x " + x + ", y " + y + ", terrain " + terrain, true); } // Base Info Key (base only) diff --git a/sounds/bosses/unicorn/Slime Squishes and Oozes Sound Effects [NRkY5ExbzF8].opus b/sounds/bosses/unicorn/Slime Squishes and Oozes Sound Effects [NRkY5ExbzF8].opus new file mode 100644 index 0000000..677fd26 Binary files /dev/null and b/sounds/bosses/unicorn/Slime Squishes and Oozes Sound Effects [NRkY5ExbzF8].opus differ diff --git a/sounds/bosses/unicorn/unicorn_falls.ogg b/sounds/bosses/unicorn/unicorn_falls.ogg new file mode 100644 index 0000000..73954c5 --- /dev/null +++ b/sounds/bosses/unicorn/unicorn_falls.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a535c89594a5804fbdfa308d4ffcbd93a020a773c0ecf7d764ca01329bffff1 +size 21411 diff --git a/sounds/bosses/unicorn/unicorn_galloping.ogg b/sounds/bosses/unicorn/unicorn_galloping.ogg new file mode 100644 index 0000000..68984b2 --- /dev/null +++ b/sounds/bosses/unicorn/unicorn_galloping.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65ff9a51b46acd1a8ee6f07110b77f965ba1c7fc82d5f068fd816016332f3be0 +size 11436 diff --git a/sounds/bosses/unicorn/unicorn_on_bridge.ogg b/sounds/bosses/unicorn/unicorn_on_bridge.ogg new file mode 100644 index 0000000..f26b414 --- /dev/null +++ b/sounds/bosses/unicorn/unicorn_on_bridge.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab192897189f63dbfc969d5a0fefb35533b8995da3bca7122c2ad2441e778267 +size 24853 diff --git a/sounds/terrain/deep_forest.ogg b/sounds/terrain/deep_forest.ogg new file mode 100644 index 0000000..4e35e78 --- /dev/null +++ b/sounds/terrain/deep_forest.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26eaf5d0fd43b318f59ffb12e69bd723fbf7ac3fd0abd8f09305ed6dd5dda70d +size 8729 diff --git a/sounds/terrain/forest.ogg b/sounds/terrain/forest.ogg new file mode 100644 index 0000000..add65d4 --- /dev/null +++ b/sounds/terrain/forest.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3052170a12f82839fc356bb1c9c7ab2573ab63478c6add860101cb3b048b9649 +size 8405 diff --git a/src/audio_utils.nvgt b/src/audio_utils.nvgt index 3a4685c..e216f36 100644 --- a/src/audio_utils.nvgt +++ b/src/audio_utils.nvgt @@ -37,6 +37,10 @@ string get_footstep_sound(int current_x, int base_end, int grass_end) return "sounds/terrain/gravel.ogg"; } else if (terrain == "snow") { return "sounds/terrain/snow.ogg"; + } else if (terrain == "forest") { + return "sounds/terrain/forest.ogg"; + } else if (terrain == "deep_forest") { + return "sounds/terrain/deep_forest.ogg"; } } @@ -57,6 +61,10 @@ string get_footstep_sound(int current_x, int base_end, int grass_end) return "sounds/terrain/snow.ogg"; } else if (terrain == "gravel") { return "sounds/terrain/gravel.ogg"; + } else if (terrain == "forest") { + return "sounds/terrain/forest.ogg"; + } else if (terrain == "deep_forest") { + return "sounds/terrain/deep_forest.ogg"; } } } diff --git a/src/bosses/adventure_system.nvgt b/src/bosses/adventure_system.nvgt new file mode 100644 index 0000000..1d91b67 --- /dev/null +++ b/src/bosses/adventure_system.nvgt @@ -0,0 +1,81 @@ +// Adventure System +// Handles triggering and managing terrain-specific adventures + +#include "src/bosses/unicorn/unicorn_boss.nvgt" + +void check_adventure_menu(int player_x) { + if (key_pressed(KEY_TAB)) { + run_adventure_menu(player_x); + } +} + +void run_adventure_menu(int player_x) { + if (player_x <= BASE_END) { + speak_with_history("No adventures available in the base.", true); + return; + } + + if (last_adventure_day == current_day) { + speak_with_history("You have already attempted an adventure today.", true); + return; + } + + string terrain = get_terrain_at_position(player_x); + MountainRange@ mountain = get_mountain_at(player_x); + + // Check available adventures based on terrain + string[] options; + int[] adventure_ids; // 1 = Unicorn + + if (mountain !is null) { + // Mountain terrain + options.insert_last("Unicorn Hunt (Mountain Boss)"); + adventure_ids.insert_last(1); + } + + if (options.length() == 0) { + speak_with_history("No adventures found in this area.", true); + return; + } + + // Show Menu + speak_with_history("Adventure Menu.", true); + int selection = 0; + speak_with_history(options[selection], true); + + while (true) { + wait(5); + + if (key_pressed(KEY_ESCAPE)) { + speak_with_history("Closed.", true); + return; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + speak_with_history(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + speak_with_history(options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + start_adventure(adventure_ids[selection]); + // Adventure has finished (it blocks until done) + // Resume main game ambience + update_ambience(true); + return; + } + } +} + +void start_adventure(int adventure_id) { + last_adventure_day = current_day; + if (adventure_id == 1) { + run_unicorn_adventure(); + } +} diff --git a/src/bosses/unicorn/unicorn_boss.nvgt b/src/bosses/unicorn/unicorn_boss.nvgt new file mode 100644 index 0000000..c4994ea --- /dev/null +++ b/src/bosses/unicorn/unicorn_boss.nvgt @@ -0,0 +1,400 @@ +// Unicorn Boss Adventure logic +// Terrain: Mountain +// Objective: Destroy the bridge supports to defeat the charging unicorn. + +class UnicornBoss { + int x; + int facing; // 0 = left, 1 = right + int health; + int speed; + timer move_timer; + int sound_handle; + bool on_bridge; + + UnicornBoss() { + reset(); + } + + void reset() { + health = 10000; + speed = UNICORN_SPEED; + facing = 0; // Start facing west (toward player) + x = 0; + on_bridge = false; + sound_handle = -1; + move_timer.restart(); + } +} + +// Adventure Arena Constants +const int UNICORN_ARENA_SIZE = 100; +const int BRIDGE_START = 45; +const int BRIDGE_END = 54; +const int BRIDGE_SUPPORT_MAX_HEALTH = 100; +const float UNICORN_SOUND_VOLUME_STEP = 2.5; // Lower = audible from further away +const int UNICORN_SPEED = 80; // ms per tile, 100 tiles * 80ms = 8 seconds per charge + +// State +UnicornBoss unicorn; +int player_arena_x = 0; +int player_arena_y = 0; // 0 = ground, >0 = jump +bool player_arena_jumping = false; +timer arena_jump_timer; +timer arena_walk_timer; +timer arena_attack_timer; +int player_arena_facing = 1; // 0 = west, 1 = east +int[] bridge_supports_health; // 2 supports: Left (start) and Right (end) +bool bridge_collapsed = false; +string current_unicorn_sound = ""; + +void init_unicorn_adventure() { + unicorn.reset(); + unicorn.x = UNICORN_ARENA_SIZE - 1; // Start at east end + + player_arena_x = 0; // Start player at west end + player_arena_y = 0; + player_arena_jumping = false; + bridge_collapsed = false; + current_unicorn_sound = ""; + + // Initialize supports + bridge_supports_health.resize(2); + bridge_supports_health[0] = BRIDGE_SUPPORT_MAX_HEALTH; // Left support at BRIDGE_START + bridge_supports_health[1] = BRIDGE_SUPPORT_MAX_HEALTH; // Right support at BRIDGE_END +} + +void cleanup_unicorn_adventure() { + p.destroy_all(); + if (unicorn.sound_handle != -1) { + p.destroy_sound(unicorn.sound_handle); + unicorn.sound_handle = -1; + } +} + +void run_unicorn_adventure() { + // Stop main game sounds + p.destroy_all(); + + init_unicorn_adventure(); + + speak_with_history("You enter a mountain pass. A massive Unicorn blocks the path! A wooden bridge spans a chasm ahead.", true); + + // Adventure Loop + while (true) { + wait(5); + + // Input Handling + if (key_pressed(KEY_ESCAPE)) { + cleanup_unicorn_adventure(); + speak_with_history("You flee the encounter.", true); + return; + } + + // Standard game keys + check_quick_slot_keys(); + check_notification_keys(); + check_speech_history_keys(); + + // Health + if (key_pressed(KEY_H)) { + speak_with_history(player_health + " health of " + max_health, true); + } + + // Coordinates + if (key_pressed(KEY_X)) { + string facing_dir = (unicorn.facing == 1) ? "east" : "west"; + string terrain = (player_arena_x >= BRIDGE_START && player_arena_x <= BRIDGE_END && !bridge_collapsed) ? "wood" : "grass"; + speak_with_history("x " + player_arena_x + ", terrain " + terrain + ". Unicorn facing " + facing_dir, true); + } + + handle_player_movement(); + handle_player_actions(); + + // Updates + update_player_jump(); + update_unicorn(); + + // Check Conditions - unicorn falls when on collapsed bridge + if (bridge_collapsed && unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END) { + play_unicorn_death_sequence(); + cleanup_unicorn_adventure(); + give_unicorn_rewards(); + return; + } + + if (player_health <= 0) { + cleanup_unicorn_adventure(); + speak_with_history("The Unicorn trampled you.", true); + // Player death will be handled by main game loop checking player_health <= 0 + return; + } + + // Audio + p.update_listener_1d(player_arena_x); + update_unicorn_audio(); + } +} + +void handle_player_movement() { + // Direction change announces + if (key_pressed(KEY_LEFT) && player_arena_facing != 0) { + player_arena_facing = 0; + speak_with_history("west", true); + arena_walk_timer.restart(); + } + if (key_pressed(KEY_RIGHT) && player_arena_facing != 1) { + player_arena_facing = 1; + speak_with_history("east", true); + arena_walk_timer.restart(); + } + + // Movement with walk timer (like main game) + if (arena_walk_timer.elapsed > walk_speed && !player_arena_jumping) { + if (key_down(KEY_LEFT) && player_arena_x > 0) { + player_arena_facing = 0; + player_arena_x--; + arena_walk_timer.restart(); + check_player_chasm_fall(); + if (player_health > 0) play_footstep_sound(); + } else if (key_down(KEY_RIGHT) && player_arena_x < UNICORN_ARENA_SIZE - 1) { + player_arena_facing = 1; + player_arena_x++; + arena_walk_timer.restart(); + check_player_chasm_fall(); + if (player_health > 0) play_footstep_sound(); + } + } + + // Jumping + if (key_pressed(KEY_UP) && !player_arena_jumping) { + player_arena_jumping = true; + arena_jump_timer.restart(); + p.play_stationary("sounds/jump.ogg", false); + player_arena_y = 1; + } +} + +void play_footstep_sound() { + if (player_arena_x >= BRIDGE_START && player_arena_x <= BRIDGE_END && !bridge_collapsed) { + p.play_stationary("sounds/terrain/wood.ogg", false); + } else { + p.play_stationary("sounds/terrain/grass.ogg", false); + } +} + +void check_player_chasm_fall() { + // If bridge is collapsed and player walks into the chasm, they fall 100 feet + if (bridge_collapsed && player_arena_x >= BRIDGE_START && player_arena_x <= BRIDGE_END) { + // Play falling sequence like falling from a 100-foot tree + timer fall_timer; + int fall_handle = -1; + int feet_fallen = 0; + int total_fall = 100; + + while (feet_fallen < total_fall) { + if (fall_timer.elapsed >= 100) { + fall_timer.restart(); + feet_fallen++; + + if (fall_handle != -1) { + p.destroy_sound(fall_handle); + } + + float height_remaining = float(total_fall - feet_fallen); + float pitch_percent = 50.0 + (50.0 * (height_remaining / float(total_fall))); + if (pitch_percent < 50.0) pitch_percent = 50.0; + if (pitch_percent > 100.0) pitch_percent = 100.0; + + fall_handle = p.play_stationary_extended("sounds/actions/falling.ogg", true, 0, 0, 0, pitch_percent); + } + wait(5); + } + + if (fall_handle != -1) { + p.destroy_sound(fall_handle); + } + p.play_stationary("sounds/actions/hit_ground.ogg", false); + player_health = 0; + } +} + +void update_player_jump() { + if (player_arena_jumping && arena_jump_timer.elapsed > 800) { + player_arena_jumping = false; + player_arena_y = 0; + play_footstep_sound(); // Land sound + } +} + +void handle_player_actions() { + // Can't attack while jumping + if (player_arena_jumping) return; + + // Attack cooldown like main game + int attack_cooldown = 1000; + if (spear_equipped) attack_cooldown = 800; + if (axe_equipped) attack_cooldown = 1600; + + if ((key_down(KEY_LCTRL) || key_down(KEY_RCTRL)) && arena_attack_timer.elapsed > attack_cooldown) { + arena_attack_timer.restart(); + + // Check for bridge supports + int target_support = -1; + + // Check Left Support (at BRIDGE_START) + if (abs(player_arena_x - BRIDGE_START) <= 1) { + target_support = 0; + } + // Check Right Support (at BRIDGE_END) + else if (abs(player_arena_x - BRIDGE_END) <= 1) { + target_support = 1; + } + + if (target_support != -1) { + if (bridge_supports_health[target_support] > 0) { + // Only axe can damage supports (like chopping trees) + if (axe_equipped) { + bridge_supports_health[target_support] -= AXE_DAMAGE; + p.play_stationary("sounds/weapons/axe_hit.ogg", false); + + if (bridge_supports_health[target_support] <= 0) { + check_bridge_collapse(); + } + } else if (spear_equipped) { + // Spear just makes sound, no damage (like hitting trees) + p.play_stationary("sounds/weapons/spear_hit.ogg", false); + } else { + // No weapon or sling - swing sound, no effect + p.play_stationary("sounds/weapons/axe_swing.ogg", false); + } + } + } else { + // Normal attack (useless vs unicorn but gives feedback) + if (abs(player_arena_x - unicorn.x) <= 1) { + p.play_stationary("sounds/weapons/axe_hit.ogg", false); + } else { + p.play_stationary("sounds/weapons/axe_swing.ogg", false); + } + } + } +} + +void check_bridge_collapse() { + // Bridge collapses when any support is destroyed + if (bridge_supports_health[0] <= 0 || bridge_supports_health[1] <= 0) { + bridge_collapsed = true; + p.play_stationary("sounds/actions/break_snare.ogg", false); + + // If player is on bridge, they fall too + if (player_arena_x >= BRIDGE_START && player_arena_x <= BRIDGE_END) { + check_player_chasm_fall(); + } + } +} + +void update_unicorn() { + if (unicorn.move_timer.elapsed >= unicorn.speed) { + unicorn.move_timer.restart(); + + // Move + if (unicorn.facing == 1) { + unicorn.x++; + if (unicorn.x >= UNICORN_ARENA_SIZE) { + unicorn.facing = 0; // Turn around + unicorn.x = UNICORN_ARENA_SIZE - 1; + } + } else { + unicorn.x--; + if (unicorn.x < 0) { + unicorn.facing = 1; // Turn around + unicorn.x = 0; + } + } + + // Bridge Logic + if (unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END && !bridge_collapsed) { + unicorn.on_bridge = true; + } else { + unicorn.on_bridge = false; + } + + // Collision with Player + if (unicorn.x == player_arena_x && player_arena_y == 0) { + player_health -= 10; + p.play_stationary("sounds/actions/hit_ground.ogg", false); + } + } +} + +void update_unicorn_audio() { + // Determine sound based on surface + string sound_file = "sounds/bosses/unicorn/unicorn_galloping.ogg"; + if (unicorn.on_bridge) { + sound_file = "sounds/bosses/unicorn/unicorn_on_bridge.ogg"; + } + + // Check if we need to switch sounds (different file or no active sound) + bool need_new_sound = (unicorn.sound_handle == -1 || !p.sound_is_active(unicorn.sound_handle) || current_unicorn_sound != sound_file); + + if (need_new_sound) { + // Stop old sound if playing + if (unicorn.sound_handle != -1) { + p.destroy_sound(unicorn.sound_handle); + } + // Start new positioned sound using shared helper + unicorn.sound_handle = play_1d_with_volume_step(sound_file, player_arena_x, unicorn.x, true, UNICORN_SOUND_VOLUME_STEP); + current_unicorn_sound = sound_file; + } else { + // Update position of existing sound + p.update_sound_1d(unicorn.sound_handle, unicorn.x); + } +} + +void play_unicorn_death_sequence() { + // Stop unicorn's normal sound + if (unicorn.sound_handle != -1) { + p.destroy_sound(unicorn.sound_handle); + unicorn.sound_handle = -1; + } + + // Simulate 100-foot fall, updating every 100ms like normal falling + timer fall_timer; + int fall_handle = -1; + int feet_fallen = 0; + int total_fall = 100; + + while (feet_fallen < total_fall) { + if (fall_timer.elapsed >= 100) { + fall_timer.restart(); + feet_fallen++; + + // Restart falling sound with decreasing pitch each foot + if (fall_handle != -1) { + p.destroy_sound(fall_handle); + } + + // Pitch ranges from 100 (start) to 50 (end) like normal falling + float height_remaining = float(total_fall - feet_fallen); + float pitch_percent = 50.0 + (50.0 * (height_remaining / float(total_fall))); + if (pitch_percent < 50.0) pitch_percent = 50.0; + if (pitch_percent > 100.0) pitch_percent = 100.0; + + fall_handle = p.play_extended_1d("sounds/actions/falling.ogg", player_arena_x, unicorn.x, 0, 0, true, 0, 0.0, 0.0, pitch_percent); + if (fall_handle != -1) { + p.update_sound_positioning_values(fall_handle, -1.0, UNICORN_SOUND_VOLUME_STEP, true); + } + } + wait(5); + } + + // Cleanup falling sound and play impact + if (fall_handle != -1) { + p.destroy_sound(fall_handle); + } + play_1d_with_volume_step("sounds/bosses/unicorn/unicorn_falls.ogg", player_arena_x, unicorn.x, false, UNICORN_SOUND_VOLUME_STEP); +} + +void give_unicorn_rewards() { + speak_with_history("Victory!", true); + favor += 50; +} diff --git a/src/environment.nvgt b/src/environment.nvgt index f855cd4..3edf697 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -687,6 +687,58 @@ void perform_search(int current_x) } return; } + + // Forest terrain - check for sticks and vines + bool is_forest_terrain = false; + string current_terrain = ""; + + // Check expanded areas for forest/deep_forest + if (expanded_area_start != -1 && current_x >= expanded_area_start && current_x <= expanded_area_end) { + // Check for mountain terrain first + MountainRange@ mountain = get_mountain_at(current_x); + if (mountain !is null) { + current_terrain = mountain.get_terrain_at(current_x); + } else { + // Regular expanded area - check terrain type + int index = current_x - expanded_area_start; + if (index >= 0 && index < int(expanded_terrain_types.length())) { + current_terrain = expanded_terrain_types[index]; + // Handle "mountain:terrain" format from older saves + if (current_terrain.find("mountain:") == 0) { + current_terrain = current_terrain.substr(9); + } + } + } + if (current_terrain == "forest" || current_terrain == "deep_forest") { + is_forest_terrain = true; + } + } + + if (is_forest_terrain) + { + bool can_find_stick = inv_sticks < get_personal_stack_limit(); + bool can_find_vine = inv_vines < get_personal_stack_limit(); + + if (!can_find_stick && !can_find_vine) { + speak_with_history("You can't carry any more sticks or vines.", true); + return; + } + + // Random choice: if both available, 50/50. If only one, find that. + bool find_stick = can_find_stick && (!can_find_vine || random(0, 1) == 0); + + if (find_stick) { + inv_sticks++; + p.play_stationary("sounds/items/stick.ogg", false); + speak_with_history("Found a stick.", true); + } else { + inv_vines++; + p.play_stationary("sounds/items/vine.ogg", false); + speak_with_history("Found a vine.", true); + } + return; + } + speak_with_history("Found nothing.", true); } diff --git a/src/player.nvgt b/src/player.nvgt index ff2db2c..ba93a3e 100644 --- a/src/player.nvgt +++ b/src/player.nvgt @@ -51,6 +51,9 @@ timer attack_timer; // Search state bool searching = false; +// Adventure state +int last_adventure_day = -1; + // Pause state bool game_paused = false; diff --git a/src/save_system.nvgt b/src/save_system.nvgt index e602566..bed46cb 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -176,6 +176,7 @@ void reset_game_state() { base_max_health = 10; max_health = 10; favor = 0.0; + last_adventure_day = -1; incense_hours_remaining = 0; incense_burning = false; blessing_speed_active = false; @@ -442,6 +443,7 @@ bool load_game_state_from_raw(const string&in rawData) { if (get_raw_number(rawData, "player_base_health", value)) base_max_health = value; if (get_raw_number(rawData, "player_max_health", value)) max_health = value; if (get_raw_number(rawData, "player_favor", value)) favor = value; + if (get_raw_number(rawData, "player_last_adventure_day", value)) last_adventure_day = value; if (get_raw_number(rawData, "incense_hours_remaining", value)) incense_hours_remaining = value; if (get_raw_bool(rawData, "incense_burning", bool_value)) incense_burning = bool_value; if (get_raw_number(rawData, "time_current_hour", value)) current_hour = value; @@ -537,6 +539,7 @@ bool save_game_state() { saveData.set("player_base_health", base_max_health); saveData.set("player_max_health", max_health); saveData.set("player_favor", favor); + saveData.set("player_last_adventure_day", last_adventure_day); saveData.set("incense_hours_remaining", incense_hours_remaining); saveData.set("incense_burning", incense_burning); @@ -567,6 +570,7 @@ bool save_game_state() { saveData.set("inventory_skin_tunics", inv_skin_tunics); saveData.set("inventory_moccasins", inv_moccasins); saveData.set("inventory_skin_pouches", inv_skin_pouches); + saveData.set("inventory_backpacks", inv_backpacks); saveData.set("inventory_small_game_types", join_string_array(inv_small_game_types)); saveData.set("storage_stones", storage_stones); @@ -596,6 +600,7 @@ bool save_game_state() { saveData.set("storage_skin_tunics", storage_skin_tunics); saveData.set("storage_moccasins", storage_moccasins); saveData.set("storage_skin_pouches", storage_skin_pouches); + saveData.set("storage_backpacks", storage_backpacks); saveData.set("storage_small_game_types", join_string_array(storage_small_game_types)); saveData.set("equipment_spear_equipped", spear_equipped); @@ -790,6 +795,7 @@ bool load_game_state() { max_health = int(get_number(saveData, "player_max_health", 10)); base_max_health = int(get_number(saveData, "player_base_health", max_health)); favor = get_number(saveData, "player_favor", 0.0); + last_adventure_day = int(get_number(saveData, "player_last_adventure_day", -1)); incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0)); incense_burning = get_bool(saveData, "incense_burning", false); if (incense_hours_remaining > 0) incense_burning = true; @@ -826,6 +832,7 @@ bool load_game_state() { inv_skin_tunics = int(get_number(saveData, "inventory_skin_tunics", 0)); inv_moccasins = int(get_number(saveData, "inventory_moccasins", 0)); inv_skin_pouches = int(get_number(saveData, "inventory_skin_pouches", 0)); + inv_backpacks = int(get_number(saveData, "inventory_backpacks", 0)); string[] loadedSmallGameTypes = get_string_list_or_split(saveData, "inventory_small_game_types"); inv_small_game_types.resize(0); @@ -868,6 +875,7 @@ bool load_game_state() { storage_skin_tunics = int(get_number(saveData, "storage_skin_tunics", 0)); storage_moccasins = int(get_number(saveData, "storage_moccasins", 0)); storage_skin_pouches = int(get_number(saveData, "storage_skin_pouches", 0)); + storage_backpacks = int(get_number(saveData, "storage_backpacks", 0)); string[] loadedStorageSmallGameTypes = get_string_list_or_split(saveData, "storage_small_game_types"); storage_small_game_types.resize(0); diff --git a/src/text_reader.nvgt b/src/text_reader.nvgt new file mode 100644 index 0000000..09edfff --- /dev/null +++ b/src/text_reader.nvgt @@ -0,0 +1,114 @@ +// text_reader.nvgt - Simple text document reader/editor using NVGT's audio_form +// Provides accessible navigation through text documents with optional editing + +#include "form.nvgt" + +// Opens a text reader/editor window with string content +// Parameters: +// content: The text content to display (can be file contents or direct string) +// title: Window title (default: "Text Reader") +// readonly: If true, text cannot be edited (default: true) +// Returns: The modified text if readonly=false and user presses OK, empty string if canceled or readonly=true +string text_reader(string content, string title = "Text Reader", bool readonly = true) { + audio_form f; + f.create_window(title, false, true); + + // Create the multiline input box + // In readonly mode, it's still navigable with arrows/home/end/etc + // In edit mode, user can modify text + int text_control = f.create_input_box( + (readonly ? "Document (read only)" : "Document (editable)"), + content, + "", // no password mask + 0, // no max length + readonly, + true, // multiline = true + true // multiline_enter = true (Ctrl+Enter for newlines) + ); + + int ok_button = -1; + int close_button = -1; + + if (readonly) { + // In readonly mode, just have a Close button + close_button = f.create_button("&Close", true, true); + } else { + // In edit mode, have OK and Cancel buttons + ok_button = f.create_button("&OK", true); + close_button = f.create_button("&Cancel", false, true); + } + + f.focus(text_control); + + // Monitor loop + while (true) { + f.monitor(); + wait(5); + + // Check if user pressed OK (edit mode only) + if (!readonly && ok_button != -1 && f.is_pressed(ok_button)) { + return f.get_text(text_control); + } + + // Check if user pressed Close/Cancel + if (close_button != -1 && f.is_pressed(close_button)) { + return ""; + } + + // Check for Escape key as alternative close method + if (key_pressed(KEY_ESCAPE)) { + return ""; + } + } + + return ""; +} + +// Opens a text reader/editor window with an array of lines +// Parameters: +// lines: Array of text lines to display +// title: Window title (default: "Text Reader") +// readonly: If true, text cannot be edited (default: true) +// Returns: The modified text if readonly=false and user presses OK, empty string if canceled or readonly=true +string text_reader_lines(string[] lines, string title = "Text Reader", bool readonly = true) { + string content = join(lines, "\n"); + return text_reader(content, title, readonly); +} + +// Convenience function to read a file and display it in the text reader +// Parameters: +// file_path: Path to the file to read +// title: Window title (default: uses file_path as title) +// readonly: If true, text cannot be edited (default: true) +// Returns: The modified text if readonly=false and user saves, empty string otherwise +string text_reader_file(string file_path, string title = "", bool readonly = true) { + file f; + if (!f.open(file_path, "rb")) { + screen_reader_speak("Failed to open file: " + file_path, true); + return ""; + } + + string content = f.read(); + f.close(); + + // Use file_path as title if no custom title provided + if (title == "") { + title = file_path; + } + + string result = text_reader(content, title, readonly); + + // If in edit mode and user pressed OK, save the file + if (!readonly && result != "") { + if (f.open(file_path, "wb")) { + f.write(result); + f.close(); + screen_reader_speak("File saved successfully", true); + return result; + } else { + screen_reader_speak("Failed to save file", true); + } + } + + return result; +} diff --git a/src/time_system.nvgt b/src/time_system.nvgt index c124437..aa36855 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -74,13 +74,17 @@ void expand_regular_area() { // Generate a single terrain type for the entire new area string terrain_type; - int terrain_roll = random(0, 2); + int terrain_roll = random(0, 4); if (terrain_roll == 0) { terrain_type = "stone"; } else if (terrain_roll == 1) { terrain_type = "grass"; - } else { + } else if (terrain_roll == 2) { terrain_type = "snow"; + } else if (terrain_roll == 3) { + terrain_type = "forest"; + } else { + terrain_type = "deep_forest"; } for (int i = 0; i < EXPANSION_SIZE; i++) { diff --git a/src/ui.nvgt b/src/ui.nvgt index 0e7c6e6..6123762 100644 --- a/src/ui.nvgt +++ b/src/ui.nvgt @@ -1,4 +1,39 @@ // UI helpers +string get_terrain_at_position(int pos_x) { + // Check for water first (streams in expanded areas or mountain streams) + if (is_position_in_water(pos_x) || is_mountain_stream_at(pos_x)) { + return "water"; + } + + // Check mountain terrain + MountainRange@ mountain = get_mountain_at(pos_x); + if (mountain !is null) { + return mountain.get_terrain_at(pos_x); + } + + // Base area + if (pos_x <= BASE_END) return "wood"; + + // Grass area + if (pos_x <= GRASS_END) return "grass"; + + // Gravel area + if (pos_x <= GRAVEL_END) return "gravel"; + + // Expanded areas + int index = pos_x - expanded_area_start; + if (index >= 0 && index < int(expanded_terrain_types.length())) { + string terrain = expanded_terrain_types[index]; + // Handle "mountain:terrain" format from older saves + if (terrain.find("mountain:") == 0) { + terrain = terrain.substr(9); + } + return terrain; + } + + return "unknown"; +} + string ui_input_box(const string title, const string prompt, const string default_value) { string result = virtual_input_box(title, prompt, default_value); show_window("Draugnorak"); diff --git a/src/world/mountains.nvgt b/src/world/mountains.nvgt index 91d2a63..d242914 100644 --- a/src/world/mountains.nvgt +++ b/src/world/mountains.nvgt @@ -46,10 +46,12 @@ class MountainRange { for (int i = 0; i < size; i++) { if (elevations[i] > 20) { terrain_types[i] = "snow"; - } else if (elevations[i] > 8) { + } else if (elevations[i] > 12) { terrain_types[i] = "stone"; + } else if (elevations[i] > 6) { + terrain_types[i] = "forest"; } else { - terrain_types[i] = "gravel"; + terrain_types[i] = "deep_forest"; } }