From 489408a1813092056e1179ee2d8268028854bde6 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 18 Jan 2026 22:25:38 -0500 Subject: [PATCH] A ton of changes including bug fixes, new craftable items, altar with favor system added. Info for player and base added. --- README.md | 3 + draugnorak.nvgt | 35 +- sounds/nature/day.ogg | 4 +- sounds/nature/night.ogg | 4 +- sounds/quests/bat1.ogg | 3 + sounds/quests/bat2.ogg | 3 + sounds/quests/bee.ogg | 3 + sounds/quests/bone1.ogg | 3 + sounds/quests/bone2.ogg | 3 + sounds/quests/bone3.ogg | 3 + sounds/quests/bone4.ogg | 3 + sounds/quests/bone5.ogg | 3 + sounds/quests/bone6.ogg | 3 + sounds/quests/bone7.ogg | 3 + sounds/quests/bone8.ogg | 3 + sounds/quests/fall.ogg | 3 + sounds/quests/footstep.ogg | 3 + sounds/quests/jump.ogg | 3 + sounds/quests/pit.ogg | 3 + sounds/quests/spear_hit.ogg | 3 + sounds/quests/spear_miss.ogg | 3 + sounds/quests/squish.ogg | 3 + src/base_system.nvgt | 39 ++ src/combat.nvgt | 6 +- src/constants.nvgt | 38 +- src/crafting.nvgt | 897 ++++++++++++++++++++++++++ src/environment.nvgt | 119 +++- src/inventory.nvgt | 893 +------------------------ src/inventory_items.nvgt | 439 +++++++++++++ src/inventory_menus.nvgt | 811 +++++++++++++++++++++++ src/notify.nvgt | 25 +- src/player.nvgt | 6 + src/quest_system.nvgt | 151 +++++ src/quests/bat_invasion_game.nvgt | 99 +++ src/quests/enchanted_melody_game.nvgt | 87 +++ src/quests/escape_from_hel_game.nvgt | 117 ++++ src/save_system.nvgt | 567 +++++++++++++++- src/time_system.nvgt | 314 +++++++-- src/ui.nvgt | 17 + src/world_state.nvgt | 231 ++++++- 40 files changed, 3910 insertions(+), 1046 deletions(-) create mode 100644 sounds/quests/bat1.ogg create mode 100644 sounds/quests/bat2.ogg create mode 100644 sounds/quests/bee.ogg create mode 100644 sounds/quests/bone1.ogg create mode 100644 sounds/quests/bone2.ogg create mode 100644 sounds/quests/bone3.ogg create mode 100644 sounds/quests/bone4.ogg create mode 100644 sounds/quests/bone5.ogg create mode 100644 sounds/quests/bone6.ogg create mode 100644 sounds/quests/bone7.ogg create mode 100644 sounds/quests/bone8.ogg create mode 100644 sounds/quests/fall.ogg create mode 100644 sounds/quests/footstep.ogg create mode 100644 sounds/quests/jump.ogg create mode 100644 sounds/quests/pit.ogg create mode 100644 sounds/quests/spear_hit.ogg create mode 100644 sounds/quests/spear_miss.ogg create mode 100644 sounds/quests/squish.ogg create mode 100644 src/base_system.nvgt create mode 100644 src/crafting.nvgt create mode 100644 src/inventory_items.nvgt create mode 100644 src/inventory_menus.nvgt create mode 100644 src/quest_system.nvgt create mode 100644 src/quests/bat_invasion_game.nvgt create mode 100644 src/quests/enchanted_melody_game.nvgt create mode 100644 src/quests/escape_from_hel_game.nvgt create mode 100644 src/ui.nvgt diff --git a/README.md b/README.md index bfae082..0023930 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,12 @@ A survival audio game built with NVGT. - **E**: Open Equipment Menu. - **A**: Open Action Menu (Place objects, feed fire). - **I**: Check Inventory. +- **P**: Character info. +- **Q**: Quests menu (base only). - **H**: Check Health. - **T**: Check Time. - **X**: Check Coordinates. +- **B**: Base info menu (base only). - **Escape**: Exit game. ## Gameplay diff --git a/draugnorak.nvgt b/draugnorak.nvgt index 24d965d..05b33c1 100644 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -8,10 +8,13 @@ sound_pool p(100); #include "src/constants.nvgt" #include "src/player.nvgt" #include "src/world_state.nvgt" +#include "src/ui.nvgt" #include "src/inventory.nvgt" +#include "src/quest_system.nvgt" #include "src/environment.nvgt" #include "src/combat.nvgt" #include "src/save_system.nvgt" +#include "src/base_system.nvgt" #include "src/time_system.nvgt" #include "src/audio_utils.nvgt" #include "src/notify.nvgt" @@ -70,9 +73,11 @@ void main() game_started = true; } else { if (has_save_game()) { - screen_reader_speak("Unable to load save.", true); + string message = last_save_error; + if (message == "") message = "Unable to load save."; + ui_info_box("Draugnorak", "Load Game", message); } else { - screen_reader_speak("No save found.", true); + ui_info_box("Draugnorak", "Load Game", "No save found."); } } } else { @@ -86,22 +91,23 @@ void main() if(key_pressed(KEY_ESCAPE)) { - int really_exit = virtual_question("Draugnorak", "Really exit?"); + int really_exit = ui_question("Draugnorak", "Really exit?"); if (really_exit == 1) { exit(); } - // Restore focus to the game window - show_window("Draugnorak"); } // Time & Environment updates update_time(); + update_crossfade(); update_environment(); update_snares(); update_streams(); update_fires(); update_zombies(); update_bandits(); + update_blessings(); + update_notifications(); // Fire damage check (only if not jumping) WorldFire@ fire_on_tile = get_fire_at(x); @@ -135,6 +141,8 @@ void main() check_action_menu(x); check_crafting_menu(x, BASE_END); check_equipment_menu(); + check_quest_menu(); + check_quick_slot_keys(); check_time_input(); check_notification_keys(); @@ -149,13 +157,9 @@ void main() screen_reader_speak(direction_label + ", x " + x + ", y " + y, true); } - // Barricade Key (base only) + // Base Info Key (base only) if (key_pressed(KEY_B)) { - if (x <= BASE_END) { - screen_reader_speak("Barricade health " + barricade_health + " of " + BARRICADE_MAX_HEALTH, true); - } else { - screen_reader_speak("You are not in the base.", true); - } + run_base_info_menu(); } // Climbing and Falling Updates @@ -259,7 +263,14 @@ void main() } // Searching Logic - if((key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT)) && search_timer.elapsed > 2000 && !searching) + bool shift_down = (key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT)); + if (!shift_down) { + if (searching) { + searching = false; + } + search_timer.restart(); + } + if (shift_down && search_timer.elapsed > 2000 && !searching) { searching = true; search_delay_timer.restart(); diff --git a/sounds/nature/day.ogg b/sounds/nature/day.ogg index 0538c22..b4c54ec 100644 --- a/sounds/nature/day.ogg +++ b/sounds/nature/day.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c26e755528865bbefe5165afff4d8751e1c45e634267614f877d0f40acae8b4d -size 1637721 +oid sha256:e15aca582a6384fac57594db0f24e1fff4b785f7444d5f52c1500c1aed29f8bc +size 796838 diff --git a/sounds/nature/night.ogg b/sounds/nature/night.ogg index f9f6042..e098de2 100644 --- a/sounds/nature/night.ogg +++ b/sounds/nature/night.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0eb20b5362c81e4416b6f7b933a68a969ce41abcb169feddbc5781fe6de0ea5 -size 9710250 +oid sha256:2c0f8df99f033bb994bfb0446a67c10498a1983e10259d3241dce90be974234e +size 821213 diff --git a/sounds/quests/bat1.ogg b/sounds/quests/bat1.ogg new file mode 100644 index 0000000..89ce4d2 --- /dev/null +++ b/sounds/quests/bat1.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f8e38f812e0640b9040ea7858af564705a6351491085d342f9810f2459bc5b9 +size 36560 diff --git a/sounds/quests/bat2.ogg b/sounds/quests/bat2.ogg new file mode 100644 index 0000000..0d3eaaf --- /dev/null +++ b/sounds/quests/bat2.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2cd0907243f5e1be4df1a3ecbc096e5f6ca7626defc07efd07ab62328a3b73b +size 37699 diff --git a/sounds/quests/bee.ogg b/sounds/quests/bee.ogg new file mode 100644 index 0000000..d7588f3 --- /dev/null +++ b/sounds/quests/bee.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b54dd289b8466be1dd2d1549820d41c30cd3f5fbbf0e6fc3a5062dde1df822d0 +size 5765 diff --git a/sounds/quests/bone1.ogg b/sounds/quests/bone1.ogg new file mode 100644 index 0000000..bf8e135 --- /dev/null +++ b/sounds/quests/bone1.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbf62500b2cb59b54dfabb80b85372737b31e682d7d3da1eb5de31141f06b7d3 +size 6487 diff --git a/sounds/quests/bone2.ogg b/sounds/quests/bone2.ogg new file mode 100644 index 0000000..576a5e2 --- /dev/null +++ b/sounds/quests/bone2.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19bc27614bc65d92f40ea14304980ff02b8b4bba8a0dfbb1c981fd116928db6a +size 6417 diff --git a/sounds/quests/bone3.ogg b/sounds/quests/bone3.ogg new file mode 100644 index 0000000..6b74bce --- /dev/null +++ b/sounds/quests/bone3.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dadf71a5d29758000ddf3b219ee8c5fb7c933eeab14e1aa35d2cf6943cc720f +size 6259 diff --git a/sounds/quests/bone4.ogg b/sounds/quests/bone4.ogg new file mode 100644 index 0000000..94fee4a --- /dev/null +++ b/sounds/quests/bone4.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61d5bb800832c6148bb57a1f615250b1e01cade9998fca1dc8c4fbad466ea1c4 +size 6418 diff --git a/sounds/quests/bone5.ogg b/sounds/quests/bone5.ogg new file mode 100644 index 0000000..d43f8e0 --- /dev/null +++ b/sounds/quests/bone5.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a33176b2e282ef76569dc155234346e9cd63f78c9d0a6686b21d09c2c53d3ef9 +size 6518 diff --git a/sounds/quests/bone6.ogg b/sounds/quests/bone6.ogg new file mode 100644 index 0000000..941e296 --- /dev/null +++ b/sounds/quests/bone6.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56e7599d09c3042b3be1f0d7670a0b509cdde07ccbce5ea804cec68275128781 +size 6091 diff --git a/sounds/quests/bone7.ogg b/sounds/quests/bone7.ogg new file mode 100644 index 0000000..af322a7 --- /dev/null +++ b/sounds/quests/bone7.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8601b92cf100f1a9bf53a9058f95888a1abcdf2784951df769ea51cdbc9f664 +size 6055 diff --git a/sounds/quests/bone8.ogg b/sounds/quests/bone8.ogg new file mode 100644 index 0000000..cbdfeff --- /dev/null +++ b/sounds/quests/bone8.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7dab845f84d4a43a5ee6ba1597d76d297cf01185f4abbd05b84ae4bbec9bf240 +size 6412 diff --git a/sounds/quests/fall.ogg b/sounds/quests/fall.ogg new file mode 100644 index 0000000..e8b47ac --- /dev/null +++ b/sounds/quests/fall.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e50fde2107befe2adeb9b2a0a46b68409b164655a927d5f20dffd83141cdd2ee +size 23430 diff --git a/sounds/quests/footstep.ogg b/sounds/quests/footstep.ogg new file mode 100644 index 0000000..4a17719 --- /dev/null +++ b/sounds/quests/footstep.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:819dab573d3f64a372d6c243795ebd57d64a91dd1fe04bdb829c423a3fa43742 +size 5084 diff --git a/sounds/quests/jump.ogg b/sounds/quests/jump.ogg new file mode 100644 index 0000000..4736677 --- /dev/null +++ b/sounds/quests/jump.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1801211a6a214a4a0c247775c76d6f2cef04341fc1d1c56c2a8d6c321e586629 +size 6763 diff --git a/sounds/quests/pit.ogg b/sounds/quests/pit.ogg new file mode 100644 index 0000000..2de0991 --- /dev/null +++ b/sounds/quests/pit.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b57bfbb0bfe37fc47be5a2f0505ca0eb456cff76663386ebce6111c0eefb2a0 +size 9926 diff --git a/sounds/quests/spear_hit.ogg b/sounds/quests/spear_hit.ogg new file mode 100644 index 0000000..fa7a008 --- /dev/null +++ b/sounds/quests/spear_hit.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4923d321fe163227095715339efcb270647681ee0b2e478ba523cc0bd4ea9e26 +size 35098 diff --git a/sounds/quests/spear_miss.ogg b/sounds/quests/spear_miss.ogg new file mode 100644 index 0000000..a82b1c5 --- /dev/null +++ b/sounds/quests/spear_miss.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e985e65e857462aa6ac17199268dd7c571c0694d7c879815a568f97f6a5c4203 +size 5683 diff --git a/sounds/quests/squish.ogg b/sounds/quests/squish.ogg new file mode 100644 index 0000000..d33ef5f --- /dev/null +++ b/sounds/quests/squish.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28129542b66a8de5bc008fa6218263c41a0d4fd2bbce99d81d396339e7f51d9a +size 6643 diff --git a/src/base_system.nvgt b/src/base_system.nvgt new file mode 100644 index 0000000..fdc9cfd --- /dev/null +++ b/src/base_system.nvgt @@ -0,0 +1,39 @@ +// Base automation helpers +int get_daily_food_requirement() { + if (residents_count <= 0) return 0; + return (residents_count + 1) / 2; +} + +void consume_food_for_residents() { + int needed = get_daily_food_requirement(); + if (needed <= 0) return; + if (storage_meat >= needed) { + storage_meat -= needed; + } else { + storage_meat = 0; + if (x <= BASE_END) { + notify("No food, residents are hungry."); + } + } +} + +void keep_base_fires_fed() { + if (residents_count <= 0) return; + if (storage_meat <= 0) return; + if (storage_sticks <= 0 && storage_logs <= 0) return; + + for (uint i = 0; i < world_fires.length(); i++) { + if (world_fires[i].position > BASE_END) continue; + if (!world_fires[i].is_burning()) continue; + if (world_fires[i].fuel_remaining > 300000) continue; + + if (storage_sticks > 0) { + storage_sticks--; + world_fires[i].add_fuel(300000); // 5 minutes + } else if (storage_logs > 0) { + storage_logs--; + world_fires[i].add_fuel(720000); // 12 minutes + } + break; + } +} diff --git a/src/combat.nvgt b/src/combat.nvgt index 8ef3491..5c01814 100644 --- a/src/combat.nvgt +++ b/src/combat.nvgt @@ -126,13 +126,13 @@ void release_sling_attack(int player_x) { return; } - // Find target in facing direction (5 tiles) + // Find target in facing direction int search_direction = (facing == 1) ? 1 : -1; int target_x = -1; bool hit_bandit = false; // Priority: Find nearest enemy (bandit or zombie) first - for (int dist = 1; dist <= 5; dist++) { + for (int dist = 1; dist <= SLING_RANGE; dist++) { int check_x = player_x + (dist * search_direction); if (check_x < 0 || check_x >= MAP_SIZE) break; @@ -155,7 +155,7 @@ void release_sling_attack(int player_x) { // If no enemy found, check for trees (but don't damage them) if (target_x == -1) { - for (int dist = 1; dist <= 5; dist++) { + for (int dist = 1; dist <= SLING_RANGE; dist++) { int check_x = player_x + (dist * search_direction); if (check_x < 0 || check_x >= MAP_SIZE) break; Tree@ tree = get_tree_at(check_x); diff --git a/src/constants.nvgt b/src/constants.nvgt index dd1edea..67c00f5 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -12,15 +12,25 @@ int expanded_area_end = -1; // Movement configuration int movetime = 400; // Time between steps/movements -int walk_speed = 400; +const int BASE_WALK_SPEED = 400; +const int MOCCASINS_WALK_SPEED = 360; +int walk_speed = BASE_WALK_SPEED; int jump_speed = 170; const int MAX_ITEM_STACK = 9; +const int POUCH_STACK_BONUS = 2; +const int BASE_STORAGE_MAX = 50; +const int BLESSING_HEAL_AMOUNT = 3; +const int BLESSING_BARRICADE_REPAIR = 20; +const int BLESSING_SPEED_DURATION = 300000; +const int BLESSING_TRIGGER_CHANCE = 10; +const int BLESSING_WALK_SPEED = 320; // Weapon damage const int SPEAR_DAMAGE = 3; const int AXE_DAMAGE = 4; const int SLING_DAMAGE_MIN = 5; const int SLING_DAMAGE_MAX = 8; +const int SLING_RANGE = 7; // Zombie settings const int ZOMBIE_HEALTH = 12; @@ -47,6 +57,18 @@ const int BARRICADE_LOG_HEALTH = 30; const int BARRICADE_STONE_COST = 5; const int BARRICADE_STONE_HEALTH = 20; +// Building costs +const int STORAGE_LOG_COST = 6; +const int STORAGE_STONE_COST = 9; +const int STORAGE_VINE_COST = 8; +const int PASTURE_LOG_COST = 8; +const int PASTURE_VINE_COST = 20; +const int STABLE_LOG_COST = 10; +const int STABLE_STONE_COST = 15; +const int STABLE_VINE_COST = 10; +const int ALTAR_STONE_COST = 9; +const int ALTAR_STICK_COST = 3; + // Bandit settings const int BANDIT_HEALTH = 4; const int BANDIT_MAX_COUNT = 3; @@ -61,3 +83,17 @@ const int BANDIT_FOOTSTEP_MAX_DISTANCE = 7; const float BANDIT_SOUND_VOLUME_STEP = 3.0; const int BANDIT_ATTACK_MAX_HEIGHT = 6; 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; + +// Stream audio +const int STREAM_SOUND_RANGE = 5; +const float STREAM_SOUND_VOLUME_STEP = 0.6; +const int QUEST_MAX_ACTIVE = 4; +const int QUEST_CHANCE_PER_FAVOR = 10; +const int QUEST_MIN_CHANCE = 5; +const double QUEST_FAVOR_PER_POINT = 0.05; +const int QUEST_STONE_SCORE = 6; +const int QUEST_LOG_SCORE = 10; +const int QUEST_SKIN_SCORE = 14; diff --git a/src/crafting.nvgt b/src/crafting.nvgt new file mode 100644 index 0000000..f082ca8 --- /dev/null +++ b/src/crafting.nvgt @@ -0,0 +1,897 @@ +// Crafting menus and recipes +void check_crafting_menu(int x, int base_end_tile) { + if (x <= base_end_tile) { + if (key_pressed(KEY_C)) { + run_crafting_menu(); + } + } +} + +void run_crafting_menu() { + screen_reader_speak("Crafting menu.", true); + + int selection = 0; + string[] categories = {"Weapons", "Tools", "Clothing", "Buildings", "Barricade"}; + int[] category_types = {0, 1, 2, 3, 4}; + if (world_altars.length() > 0) { + categories.insert_last("Altar"); + category_types.insert_last(5); + } + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= categories.length()) selection = 0; + screen_reader_speak(categories[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = categories.length() - 1; + screen_reader_speak(categories[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + int category = category_types[selection]; + if (category == 0) run_weapons_menu(); + else if (category == 1) run_tools_menu(); + else if (category == 2) run_clothing_menu(); + else if (category == 3) run_buildings_menu(); + else if (category == 4) run_barricade_menu(); + else if (category == 5) run_altar_menu(); + break; + } + } +} + +void run_weapons_menu() { + screen_reader_speak("Weapons.", true); + + int selection = 0; + string[] options = { + "Spear (1 Stick, 1 Vine, 1 Stone) [Requires Knife]", + "Sling (1 Skin, 2 Vines)" + }; + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + if (selection == 0) craft_spear(); + else if (selection == 1) craft_sling(); + break; + } + } +} + +void run_tools_menu() { + screen_reader_speak("Tools.", true); + + int selection = 0; + string[] options = { + "Stone Knife (2 Stones)", + "Snare (1 Stick, 2 Vines)", + "Stone Axe (1 Stick, 1 Vine, 2 Stones) [Requires Knife]", + "Fishing Pole (1 Stick, 2 Vines)", + "Rope (3 Vines)", + "Reed Basket (3 Reeds)", + "Clay Pot (3 Clay)", + "Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]" + }; + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + if (selection == 0) craft_knife(); + else if (selection == 1) craft_snare(); + else if (selection == 2) craft_axe(); + else if (selection == 3) craft_fishing_pole(); + else if (selection == 4) craft_rope(); + else if (selection == 5) craft_reed_basket(); + else if (selection == 6) craft_clay_pot(); + else if (selection == 7) butcher_small_game(); + break; + } + } +} + +void run_clothing_menu() { + screen_reader_speak("Clothing.", true); + + int selection = 0; + string[] options = { + "Skin Hat (1 Skin, 1 Vine)", + "Skin Gloves (1 Skin, 1 Vine)", + "Skin Pants (6 Skins, 3 Vines)", + "Skin Tunic (4 Skins, 2 Vines)", + "Moccasins (2 Skins, 1 Vine)", + "Skin Pouch (2 Skins, 1 Vine)" + }; + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + if (selection == 0) craft_skin_hat(); + else if (selection == 1) craft_skin_gloves(); + else if (selection == 2) craft_skin_pants(); + else if (selection == 3) craft_skin_tunic(); + else if (selection == 4) craft_moccasins(); + else if (selection == 5) craft_skin_pouch(); + break; + } + } +} + +void run_buildings_menu() { + screen_reader_speak("Buildings.", true); + + int selection = 0; + string[] options = { + "Firepit (9 Stones)", + "Fire (2 Sticks, 1 Log) [Requires Firepit]", + "Herb Garden (9 Stones, 3 Vines, 2 Logs) [Base Only]", + "Storage (6 Logs, 9 Stones, 8 Vines) [Base Only]", + "Pasture (8 Logs, 20 Vines) [Base Only]", + "Stable (10 Logs, 15 Stones, 10 Vines) [Base Only]", + "Altar (9 Stones, 3 Sticks) [Base Only]" + }; + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + if (selection == 0) craft_firepit(); + else if (selection == 1) craft_campfire(); + else if (selection == 2) craft_herb_garden(); + else if (selection == 3) craft_storage(); + else if (selection == 4) craft_pasture(); + else if (selection == 5) craft_stable(); + else if (selection == 6) craft_altar(); + break; + } + } +} + +void run_barricade_menu() { + if (barricade_health >= BARRICADE_MAX_HEALTH) { + screen_reader_speak("Barricade is already at full health.", true); + return; + } + + screen_reader_speak("Barricade.", true); + + int selection = 0; + string[] options; + int[] action_types; // 0 = sticks, 1 = vines, 2 = log, 3 = stones + + if (inv_sticks >= BARRICADE_STICK_COST) { + options.insert_last("Reinforce with sticks (" + BARRICADE_STICK_COST + " sticks, +" + BARRICADE_STICK_HEALTH + " health)"); + action_types.insert_last(0); + } + if (inv_vines >= BARRICADE_VINE_COST) { + options.insert_last("Reinforce with vines (" + BARRICADE_VINE_COST + " vines, +" + BARRICADE_VINE_HEALTH + " health)"); + action_types.insert_last(1); + } + if (inv_logs >= BARRICADE_LOG_COST) { + options.insert_last("Reinforce with log (" + BARRICADE_LOG_COST + " log, +" + BARRICADE_LOG_HEALTH + " health)"); + action_types.insert_last(2); + } + if (inv_stones >= BARRICADE_STONE_COST) { + options.insert_last("Reinforce with stones (" + BARRICADE_STONE_COST + " stones, +" + BARRICADE_STONE_HEALTH + " health)"); + action_types.insert_last(3); + } + + if (options.length() == 0) { + screen_reader_speak("No materials to reinforce the barricade.", true); + return; + } + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + int action = action_types[selection]; + if (action == 0) reinforce_barricade_with_sticks(); + else if (action == 1) reinforce_barricade_with_vines(); + else if (action == 2) reinforce_barricade_with_log(); + else if (action == 3) reinforce_barricade_with_stones(); + break; + } + } +} + +void simulate_crafting() { + screen_reader_speak("Crafting...", true); + timer t; + int duration = 4000; + int next_sound = 0; + + while(t.elapsed < duration) { + if(t.elapsed > next_sound) { + float pitch = random(85, 115); + p.play_stationary_extended("sounds/crafting.ogg", false, 0, 0, 0, pitch); + next_sound = t.elapsed + 800; + } + wait(5); + menu_background_tick(); + } + p.play_stationary("sounds/crafting_complete.ogg", false); +} + +void craft_knife() { + string missing = ""; + if (inv_stones < 2) missing += "2 stones "; + + if (missing == "") { + if (inv_knives >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more stone knives.", true); + return; + } + simulate_crafting(); + inv_stones -= 2; + inv_knives++; + screen_reader_speak("Crafted a Stone Knife.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_spear() { + string missing = ""; + if (inv_knives < 1) missing += "Stone Knife "; + if (inv_sticks < 1) missing += "1 stick "; + if (inv_vines < 1) missing += "1 vine "; + if (inv_stones < 1) missing += "1 stone "; + + if (missing == "") { + if (inv_spears >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more spears.", true); + return; + } + simulate_crafting(); + inv_sticks--; + inv_vines--; + inv_stones--; + inv_spears++; + screen_reader_speak("Crafted a Spear.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_sling() { + string missing = ""; + if (inv_skins < 1) missing += "1 skin "; + if (inv_vines < 2) missing += "2 vines "; + + if (missing == "") { + if (inv_slings >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more slings.", true); + return; + } + simulate_crafting(); + inv_skins--; + inv_vines -= 2; + inv_slings++; + screen_reader_speak("Crafted a Sling.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_skin_hat() { + string missing = ""; + if (inv_skins < 1) missing += "1 skin "; + if (inv_vines < 1) missing += "1 vine "; + + if (missing == "") { + if (inv_skin_hats >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more skin hats.", true); + return; + } + simulate_crafting(); + inv_skins--; + inv_vines--; + inv_skin_hats++; + screen_reader_speak("Crafted a Skin Hat.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_skin_gloves() { + string missing = ""; + if (inv_skins < 1) missing += "1 skin "; + if (inv_vines < 1) missing += "1 vine "; + + if (missing == "") { + if (inv_skin_gloves >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more skin gloves.", true); + return; + } + simulate_crafting(); + inv_skins--; + inv_vines--; + inv_skin_gloves++; + screen_reader_speak("Crafted Skin Gloves.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_skin_pants() { + string missing = ""; + if (inv_skins < 6) missing += "6 skins "; + if (inv_vines < 3) missing += "3 vines "; + + if (missing == "") { + if (inv_skin_pants >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more skin pants.", true); + return; + } + simulate_crafting(); + inv_skins -= 6; + inv_vines -= 3; + inv_skin_pants++; + screen_reader_speak("Crafted Skin Pants.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_skin_tunic() { + string missing = ""; + if (inv_skins < 4) missing += "4 skins "; + if (inv_vines < 2) missing += "2 vines "; + + if (missing == "") { + if (inv_skin_tunics >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more skin tunics.", true); + return; + } + simulate_crafting(); + inv_skins -= 4; + inv_vines -= 2; + inv_skin_tunics++; + screen_reader_speak("Crafted a Skin Tunic.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_moccasins() { + string missing = ""; + if (inv_skins < 2) missing += "2 skins "; + if (inv_vines < 1) missing += "1 vine "; + + if (missing == "") { + if (inv_moccasins >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more moccasins.", true); + return; + } + simulate_crafting(); + inv_skins -= 2; + inv_vines--; + inv_moccasins++; + screen_reader_speak("Crafted moccasins.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_skin_pouch() { + string missing = ""; + if (inv_skins < 2) missing += "2 skins "; + if (inv_vines < 1) missing += "1 vine "; + + if (missing == "") { + if (inv_skin_pouches >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more skin pouches.", true); + return; + } + simulate_crafting(); + inv_skins -= 2; + inv_vines--; + inv_skin_pouches++; + screen_reader_speak("Crafted a Skin Pouch.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_snare() { + string missing = ""; + if (inv_sticks < 1) missing += "1 stick "; + if (inv_vines < 2) missing += "2 vines "; + + if (missing == "") { + if (inv_snares >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more snares.", true); + return; + } + simulate_crafting(); + inv_sticks--; + inv_vines -= 2; + inv_snares++; + screen_reader_speak("Crafted a Snare.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_axe() { + string missing = ""; + if (inv_knives < 1) missing += "Stone Knife "; + if (inv_sticks < 1) missing += "1 stick "; + if (inv_vines < 1) missing += "1 vine "; + if (inv_stones < 2) missing += "2 stones "; + + if (missing == "") { + if (inv_axes >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more stone axes.", true); + return; + } + simulate_crafting(); + inv_sticks--; + inv_vines--; + inv_stones -= 2; + inv_axes++; + screen_reader_speak("Crafted a Stone Axe.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_firepit() { + // Check if there's already a firepit here + if (get_firepit_at(x) != null) { + screen_reader_speak("There is already a firepit here.", true); + return; + } + + string missing = ""; + if (inv_stones < 9) missing += "9 stones "; + + if (missing == "") { + simulate_crafting(); + inv_stones -= 9; + add_world_firepit(x); + screen_reader_speak("Firepit built here.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_campfire() { + // Check if there's a firepit within 2 tiles + WorldFirepit@ firepit = get_firepit_near(x, 2); + if (firepit == null) { + screen_reader_speak("You need a firepit within 2 tiles to build a fire.", true); + return; + } + + string missing = ""; + if (inv_logs < 1) missing += "1 log "; + if (inv_sticks < 2) missing += "2 sticks "; + + if (missing == "") { + simulate_crafting(); + inv_logs--; + inv_sticks -= 2; + // Build the fire at the firepit location, not player location + add_world_fire(firepit.position); + screen_reader_speak("Fire built at firepit.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_herb_garden() { + // Can only build in base area + if (x > BASE_END) { + screen_reader_speak("Herb garden can only be built in the base area.", true); + return; + } + + // Check if there's already an herb garden in the base + if (get_herb_garden_at_base() != null) { + screen_reader_speak("There is already an herb garden in the base.", true); + return; + } + + string missing = ""; + if (inv_stones < 9) missing += "9 stones "; + if (inv_vines < 3) missing += "3 vines "; + if (inv_logs < 2) missing += "2 logs "; + + if (missing == "") { + simulate_crafting(); + inv_stones -= 9; + inv_vines -= 3; + inv_logs -= 2; + add_world_herb_garden(x); + screen_reader_speak("Herb garden built. The base now heals faster.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_storage() { + if (x > BASE_END) { + screen_reader_speak("Storage must be built in the base.", true); + return; + } + if (world_storages.length() > 0) { + screen_reader_speak("Storage already built.", true); + return; + } + string missing = ""; + if (inv_logs < STORAGE_LOG_COST) missing += STORAGE_LOG_COST + " logs "; + if (inv_stones < STORAGE_STONE_COST) missing += STORAGE_STONE_COST + " stones "; + if (inv_vines < STORAGE_VINE_COST) missing += STORAGE_VINE_COST + " vines "; + + if (missing == "") { + simulate_crafting(); + inv_logs -= STORAGE_LOG_COST; + inv_stones -= STORAGE_STONE_COST; + inv_vines -= STORAGE_VINE_COST; + add_world_storage(x); + screen_reader_speak("Storage built.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_pasture() { + if (x > BASE_END) { + screen_reader_speak("Pasture must be built in the base.", true); + return; + } + if (world_pastures.length() > 0) { + screen_reader_speak("Pasture already built.", true); + return; + } + string missing = ""; + if (inv_logs < PASTURE_LOG_COST) missing += PASTURE_LOG_COST + " logs "; + if (inv_vines < PASTURE_VINE_COST) missing += PASTURE_VINE_COST + " vines "; + + if (missing == "") { + simulate_crafting(); + inv_logs -= PASTURE_LOG_COST; + inv_vines -= PASTURE_VINE_COST; + add_world_pasture(x); + screen_reader_speak("Pasture built.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_stable() { + if (x > BASE_END) { + screen_reader_speak("Stable must be built in the base.", true); + return; + } + if (world_stables.length() > 0) { + screen_reader_speak("Stable already built.", true); + return; + } + string missing = ""; + if (inv_logs < STABLE_LOG_COST) missing += STABLE_LOG_COST + " logs "; + if (inv_stones < STABLE_STONE_COST) missing += STABLE_STONE_COST + " stones "; + if (inv_vines < STABLE_VINE_COST) missing += STABLE_VINE_COST + " vines "; + + if (missing == "") { + simulate_crafting(); + inv_logs -= STABLE_LOG_COST; + inv_stones -= STABLE_STONE_COST; + inv_vines -= STABLE_VINE_COST; + add_world_stable(x); + screen_reader_speak("Stable built.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_altar() { + if (x > BASE_END) { + screen_reader_speak("Altar must be built in the base.", true); + return; + } + if (world_altars.length() > 0) { + screen_reader_speak("Altar already built.", true); + return; + } + string missing = ""; + if (inv_stones < ALTAR_STONE_COST) missing += ALTAR_STONE_COST + " stones "; + if (inv_sticks < ALTAR_STICK_COST) missing += ALTAR_STICK_COST + " sticks "; + + if (missing == "") { + simulate_crafting(); + inv_stones -= ALTAR_STONE_COST; + inv_sticks -= ALTAR_STICK_COST; + add_world_altar(x); + screen_reader_speak("Altar built.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void reinforce_barricade_with_sticks() { + if (barricade_health >= BARRICADE_MAX_HEALTH) { + screen_reader_speak("Barricade is already at full health.", true); + return; + } + if (inv_sticks < BARRICADE_STICK_COST) { + screen_reader_speak("Not enough sticks.", true); + return; + } + + simulate_crafting(); + inv_sticks -= BARRICADE_STICK_COST; + int gained = add_barricade_health(BARRICADE_STICK_HEALTH); + screen_reader_speak("Reinforced barricade with sticks. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); +} + +void reinforce_barricade_with_vines() { + if (barricade_health >= BARRICADE_MAX_HEALTH) { + screen_reader_speak("Barricade is already at full health.", true); + return; + } + if (inv_vines < BARRICADE_VINE_COST) { + screen_reader_speak("Not enough vines.", true); + return; + } + + simulate_crafting(); + inv_vines -= BARRICADE_VINE_COST; + int gained = add_barricade_health(BARRICADE_VINE_HEALTH); + screen_reader_speak("Reinforced barricade with vines. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); +} + +void reinforce_barricade_with_log() { + if (barricade_health >= BARRICADE_MAX_HEALTH) { + screen_reader_speak("Barricade is already at full health.", true); + return; + } + if (inv_logs < BARRICADE_LOG_COST) { + screen_reader_speak("Not enough logs.", true); + return; + } + + simulate_crafting(); + inv_logs -= BARRICADE_LOG_COST; + int gained = add_barricade_health(BARRICADE_LOG_HEALTH); + screen_reader_speak("Reinforced barricade with log. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); +} + +void reinforce_barricade_with_stones() { + if (barricade_health >= BARRICADE_MAX_HEALTH) { + screen_reader_speak("Barricade is already at full health.", true); + return; + } + if (inv_stones < BARRICADE_STONE_COST) { + screen_reader_speak("Not enough stones.", true); + return; + } + + simulate_crafting(); + inv_stones -= BARRICADE_STONE_COST; + int gained = add_barricade_health(BARRICADE_STONE_HEALTH); + screen_reader_speak("Reinforced barricade with stones. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); +} + +void craft_fishing_pole() { + string missing = ""; + if (inv_sticks < 1) missing += "1 stick "; + if (inv_vines < 2) missing += "2 vines "; + + if (missing == "") { + if (inv_fishing_poles >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more fishing poles.", true); + return; + } + simulate_crafting(); + inv_sticks--; + inv_vines -= 2; + inv_fishing_poles++; + screen_reader_speak("Crafted a Fishing Pole.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_rope() { + string missing = ""; + if (inv_vines < 3) missing += "3 vines "; + + if (missing == "") { + if (inv_ropes >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more rope.", true); + return; + } + simulate_crafting(); + inv_vines -= 3; + inv_ropes++; + screen_reader_speak("Crafted rope.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_reed_basket() { + string missing = ""; + if (inv_reeds < 3) missing += "3 reeds "; + + if (missing == "") { + if (inv_reed_baskets >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more reed baskets.", true); + return; + } + simulate_crafting(); + inv_reeds -= 3; + inv_reed_baskets++; + screen_reader_speak("Crafted a reed basket.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void craft_clay_pot() { + string missing = ""; + if (inv_clay < 3) missing += "3 clay "; + + // Check for fire within 3 tiles (can hear it) + WorldFire@ fire = get_fire_within_range(x, 3); + if (fire == null) { + screen_reader_speak("You need a fire within 3 tiles to craft a clay pot.", true); + return; + } + + if (missing == "") { + if (inv_clay_pots >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more clay pots.", true); + return; + } + simulate_crafting(); + inv_clay -= 3; + inv_clay_pots++; + screen_reader_speak("Crafted a clay pot.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + +void butcher_small_game() { + string missing = ""; + + // Check for knife + if (inv_knives < 1) missing += "Stone Knife "; + + // Check for small game + if (inv_small_game < 1) missing += "Small Game "; + + // Check for fire within 3 tiles (can hear it) + WorldFire@ fire = get_fire_within_range(x, 3); + if (fire == null) { + screen_reader_speak("You need a fire within 3 tiles to butcher.", true); + return; + } + + if (missing == "") { + if (inv_meat >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more meat.", true); + return; + } + if (inv_skins >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more skins.", true); + return; + } + simulate_crafting(); + + // Get the type of game we're butchering (first in the list) + string game_type = inv_small_game_types[0]; + inv_small_game_types.remove_at(0); + + inv_small_game--; + inv_meat++; + inv_skins++; + + screen_reader_speak("Butchered " + game_type + ". Got 1 meat and 1 skin.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} diff --git a/src/environment.nvgt b/src/environment.nvgt index 97c3f24..155b515 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -132,10 +132,28 @@ class Tree { } Tree@[] trees; +bool tree_too_close(int pos) { + for (uint i = 0; i < trees.length(); i++) { + int distance = trees[i].position - pos; + if (distance < 0) distance = -distance; + if (distance <= 5) { + return true; + } + } + return false; +} + void spawn_trees(int grass_start, int grass_end) { - int pos = random(grass_start, grass_end); - Tree@ t = Tree(pos); - trees.insert_last(t); + int attempts = 10; + for (int i = 0; i < attempts; i++) { + int pos = random(grass_start, grass_end); + if (tree_too_close(pos)) { + continue; + } + Tree@ t = Tree(pos); + trees.insert_last(t); + return; + } } void update_environment() { @@ -225,11 +243,11 @@ void perform_search(int current_x) WorldSnare@ s = get_snare_at(check_x); if (s != null) { if (s.has_catch) { - if (inv_small_game >= MAX_ITEM_STACK) { + if (inv_small_game >= get_personal_stack_limit()) { screen_reader_speak("You can't carry any more small game.", true); return; } - if (inv_snares >= MAX_ITEM_STACK) { + if (inv_snares >= get_personal_stack_limit()) { screen_reader_speak("You can't carry any more snares.", true); return; } @@ -238,7 +256,7 @@ void perform_search(int current_x) inv_snares++; // Recover snare screen_reader_speak("Collected " + s.catch_type + " and snare.", true); } else { - if (inv_snares >= MAX_ITEM_STACK) { + if (inv_snares >= get_personal_stack_limit()) { screen_reader_speak("You can't carry any more snares.", true); return; } @@ -251,7 +269,12 @@ void perform_search(int current_x) } } - // Stream banks - Clay (within stream sound range, but not in water) + if (random(1, 100) <= 10) { + screen_reader_speak("Found nothing.", true); + return; + } + + // Stream banks - Clay or reeds (within 3 tiles of stream, but not in water) bool near_stream_bank = false; for (uint i = 0; i < world_streams.length(); i++) { if (world_streams[i].contains_position(current_x)) { @@ -267,10 +290,33 @@ void perform_search(int current_x) } if (near_stream_bank) { - if (inv_clay < MAX_ITEM_STACK) { + bool found_reed = random(1, 100) <= 30; + if (found_reed) { + if (inv_reeds < get_personal_stack_limit()) { + inv_reeds++; + p.play_stationary("sounds/items/stick.ogg", false); + screen_reader_speak("Found a reed.", true); + return; + } + } else { + if (inv_clay < get_personal_stack_limit()) { + inv_clay++; + p.play_stationary("sounds/items/clay.ogg", false); + screen_reader_speak("Found clay.", true); + return; + } + } + + if (!found_reed && inv_reeds < get_personal_stack_limit()) { + inv_reeds++; + p.play_stationary("sounds/items/stick.ogg", false); + screen_reader_speak("Found a reed.", true); + } else if (found_reed && inv_clay < get_personal_stack_limit()) { inv_clay++; p.play_stationary("sounds/items/clay.ogg", false); screen_reader_speak("Found clay.", true); + } else if (found_reed) { + screen_reader_speak("You can't carry any more reeds.", true); } else { screen_reader_speak("You can't carry any more clay.", true); } @@ -309,28 +355,47 @@ void perform_search(int current_x) if(nearest.sticks > 0 || nearest.vines > 0) { bool find_stick = (nearest.vines <= 0) || (nearest.sticks > 0 && random(0, 1) == 0); - - if(find_stick) - { - if (inv_sticks >= MAX_ITEM_STACK) { - screen_reader_speak("You can't carry any more sticks.", true); - return; + bool took_item = false; + + if (find_stick) { + if (nearest.sticks > 0 && inv_sticks < get_personal_stack_limit()) { + nearest.sticks--; + inv_sticks++; + p.play_stationary("sounds/items/stick.ogg", false); + screen_reader_speak("Found a stick.", true); + took_item = true; + } else if (nearest.vines > 0 && inv_vines < get_personal_stack_limit()) { + nearest.vines--; + inv_vines++; + p.play_stationary("sounds/items/vine.ogg", false); + screen_reader_speak("Found a vine.", true); + took_item = true; + } + } else { + if (nearest.vines > 0 && inv_vines < get_personal_stack_limit()) { + nearest.vines--; + inv_vines++; + p.play_stationary("sounds/items/vine.ogg", false); + screen_reader_speak("Found a vine.", true); + took_item = true; + } else if (nearest.sticks > 0 && inv_sticks < get_personal_stack_limit()) { + nearest.sticks--; + inv_sticks++; + p.play_stationary("sounds/items/stick.ogg", false); + screen_reader_speak("Found a stick.", true); + took_item = true; } - nearest.sticks--; - inv_sticks++; - p.play_stationary("sounds/items/stick.ogg", false); - screen_reader_speak("Found a stick.", true); } - else - { - if (inv_vines >= MAX_ITEM_STACK) { + + if (!took_item) { + if (nearest.sticks > 0 && nearest.vines > 0) { + screen_reader_speak("You can't carry any more sticks or vines.", true); + } else if (nearest.sticks > 0) { + screen_reader_speak("You can't carry any more sticks.", true); + } else { screen_reader_speak("You can't carry any more vines.", true); - return; } - nearest.vines--; - inv_vines++; - p.play_stationary("sounds/items/vine.ogg", false); - screen_reader_speak("Found a vine.", true); + return; } if(nearest.sticks == 0 && nearest.vines == 0) { @@ -349,7 +414,7 @@ void perform_search(int current_x) // Gravel Area - Stones (20-34) if (current_x >= 20 && current_x <= 34) { - if (inv_stones < MAX_ITEM_STACK) + if (inv_stones < get_personal_stack_limit()) { inv_stones++; p.play_stationary("sounds/items/stone.ogg", false); diff --git a/src/inventory.nvgt b/src/inventory.nvgt index d89afe6..625756e 100644 --- a/src/inventory.nvgt +++ b/src/inventory.nvgt @@ -1,889 +1,4 @@ -// Inventory -int inv_stones = 0; -int inv_sticks = 0; -int inv_vines = 0; -int inv_logs = 0; -int inv_clay = 0; -int inv_small_game = 0; // Total small game caught (any type) -string[] inv_small_game_types; // Array to track what types of small game we have - -int inv_meat = 0; -int inv_skins = 0; - -int inv_spears = 0; -int inv_snares = 0; -int inv_axes = 0; -int inv_knives = 0; -int inv_fishing_poles = 0; -int inv_slings = 0; - -bool spear_equipped = false; -bool axe_equipped = false; -bool sling_equipped = false; - -int add_to_stack(int current, int amount) { - if (amount <= 0) return 0; - int space = MAX_ITEM_STACK - current; - if (space <= 0) return 0; - if (amount > space) return space; - return amount; -} - -void check_crafting_menu(int x, int base_end_tile) { - if (x <= base_end_tile) { - if (key_pressed(KEY_C)) { - run_crafting_menu(); - } - } -} - -void check_inventory_keys(int x) { - if (key_pressed(KEY_I)) { - run_inventory_menu(); - } -} - -void check_action_menu(int x) { - if (key_pressed(KEY_A)) { - run_action_menu(x); - } -} - -void menu_background_tick() { - update_time(); - update_environment(); - update_snares(); - update_fires(); - update_zombies(); - - // Fire damage check (only if not jumping) - WorldFire@ fire_on_tile = get_fire_at(x); - if (fire_on_tile != null && !jumping && fire_damage_timer.elapsed > 1000) { - player_health--; - fire_damage_timer.restart(); - screen_reader_speak("Burning! " + player_health + " health remaining.", true); - } - - // Healing in base area - if (x <= BASE_END && player_health < max_health) { - WorldHerbGarden@ herb_garden = get_herb_garden_at_base(); - int heal_interval = (herb_garden != null) ? 30000 : 150000; // 30 seconds with garden, 2.5 minutes without - - if (healing_timer.elapsed > heal_interval) { - player_health++; - healing_timer.restart(); - screen_reader_speak(player_health + " health.", true); - } - } - - // Death check - if (player_health <= 0) { - screen_reader_speak("You have died.", true); - wait(2000); - exit(); - } -} - -void show_inventory() { - string info = "Inventory: "; - info += inv_sticks + " sticks, "; - info += inv_vines + " vines, "; - info += inv_stones + " stones, "; - info += inv_logs + " logs, "; - info += inv_clay + " clay, "; - info += inv_small_game + " small game, "; - info += inv_meat + " meat, "; - info += inv_skins + " skins. "; - info += "Tools: " + inv_spears + " spears, " + inv_slings + " slings, " + inv_axes + " axes, " + inv_snares + " snares, " + inv_knives + " knives, " + inv_fishing_poles + " fishing poles."; - screen_reader_speak(info, true); -} - -void run_inventory_menu() { - screen_reader_speak("Inventory menu.", true); - - int selection = 0; - string[] options = { - "Sticks: " + inv_sticks, - "Vines: " + inv_vines, - "Stones: " + inv_stones, - "Logs: " + inv_logs, - "Clay: " + inv_clay, - "Small Game: " + inv_small_game, - "Meat: " + inv_meat, - "Skins: " + inv_skins, - "Spears: " + inv_spears, - "Slings: " + inv_slings, - "Axes: " + inv_axes, - "Snares: " + inv_snares, - "Knives: " + inv_knives, - "Fishing Poles: " + inv_fishing_poles - }; - - while(true) { - wait(5); - menu_background_tick(); - if (key_pressed(KEY_ESCAPE)) { - screen_reader_speak("Closed.", true); - break; - } - - if (key_pressed(KEY_DOWN)) { - selection++; - if (selection >= options.length()) selection = 0; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - screen_reader_speak(options[selection], true); - } - } -} - -void try_place_snare(int x) { - if (inv_snares > 0) { - // Prevent placing if one already exists here - if (get_snare_at(x) != null) { - screen_reader_speak("There is already a snare here.", true); - return; - } - - inv_snares--; - add_world_snare(x); - screen_reader_speak("Snare set.", true); - } else { - screen_reader_speak("No snares to place.", true); - } -} - -void try_feed_fire_stick(WorldFire@ fire) { - if (inv_sticks > 0 && fire != null) { - inv_sticks--; - fire.add_fuel(300000); // 5 minutes - screen_reader_speak("You dump an arm load of sticks into the fire.", true); - p.play_stationary("sounds/actions/fed_fire.ogg", false); - } -} - -void try_feed_fire_vine(WorldFire@ fire) { - if (inv_vines > 0 && fire != null) { - inv_vines--; - fire.add_fuel(60000); // 1 minute - screen_reader_speak("You toss a fiew vines and leaves into the fire.", true); - p.play_stationary("sounds/actions/fed_fire.ogg", false); - } -} - -void try_feed_fire_log(WorldFire@ fire) { - if (inv_logs > 0 && fire != null) { - inv_logs--; - fire.add_fuel(720000); // 12 minutes - screen_reader_speak("You heave a log into the fire.", true); - p.play_stationary("sounds/actions/fed_fire.ogg", 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) { - screen_reader_speak("Nothing to equip.", true); - } else { - run_equipment_menu(); - } - } -} - -void run_action_menu(int x) { - screen_reader_speak("Action menu.", true); - - int selection = 0; - string[] options; - int[] action_types; // Track what action each option corresponds to - - // Check if fire is nearby - WorldFire@ nearby_fire = get_fire_near(x); - bool can_feed_fire = nearby_fire != null; - - // Build menu options dynamically - options.insert_last("Place Snare"); - action_types.insert_last(0); - - if (can_feed_fire) { - if (inv_sticks > 0) { - options.insert_last("Feed fire with stick"); - action_types.insert_last(1); - } - if (inv_vines > 0) { - options.insert_last("Feed fire with vine"); - action_types.insert_last(2); - } - if (inv_logs > 0) { - options.insert_last("Feed fire with log"); - action_types.insert_last(3); - } - } - - while(true) { - wait(5); - menu_background_tick(); - if (key_pressed(KEY_ESCAPE)) { - screen_reader_speak("Closed.", true); - break; - } - - if (key_pressed(KEY_DOWN)) { - selection++; - if (selection >= options.length()) selection = 0; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_RETURN)) { - int action = action_types[selection]; - if (action == 0) { - try_place_snare(x); - } else if (action == 1) { - try_feed_fire_stick(nearby_fire); - } else if (action == 2) { - try_feed_fire_vine(nearby_fire); - } else if (action == 3) { - try_feed_fire_log(nearby_fire); - } - break; - } - } -} - -void run_crafting_menu() { - screen_reader_speak("Crafting menu.", true); - - int selection = 0; - string[] categories = {"Weapons", "Tools", "Buildings", "Barricade"}; - - while(true) { - wait(5); - menu_background_tick(); - if (key_pressed(KEY_ESCAPE)) { - screen_reader_speak("Closed.", true); - break; - } - - if (key_pressed(KEY_DOWN)) { - selection++; - if (selection >= categories.length()) selection = 0; - screen_reader_speak(categories[selection], true); - } - - if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = categories.length() - 1; - screen_reader_speak(categories[selection], true); - } - - if (key_pressed(KEY_RETURN)) { - if (selection == 0) run_weapons_menu(); - else if (selection == 1) run_tools_menu(); - else if (selection == 2) run_buildings_menu(); - else if (selection == 3) run_barricade_menu(); - break; - } - } -} - -void run_weapons_menu() { - screen_reader_speak("Weapons.", true); - - int selection = 0; - string[] options = { - "Spear (1 Stick, 1 Vine, 1 Stone) [Requires Knife]", - "Sling (1 Skin, 2 Vines)" - }; - - while(true) { - wait(5); - menu_background_tick(); - if (key_pressed(KEY_ESCAPE)) { - screen_reader_speak("Closed.", true); - break; - } - - if (key_pressed(KEY_DOWN)) { - selection++; - if (selection >= options.length()) selection = 0; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_RETURN)) { - if (selection == 0) craft_spear(); - else if (selection == 1) craft_sling(); - break; - } - } -} - -void run_tools_menu() { - screen_reader_speak("Tools.", true); - - int selection = 0; - string[] options = { - "Stone Knife (2 Stones)", - "Snare (1 Stick, 2 Vines)", - "Stone Axe (1 Stick, 1 Vine, 2 Stones) [Requires Knife]", - "Fishing Pole (1 Stick, 2 Vines)", - "Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]" - }; - - while(true) { - wait(5); - menu_background_tick(); - if (key_pressed(KEY_ESCAPE)) { - screen_reader_speak("Closed.", true); - break; - } - - if (key_pressed(KEY_DOWN)) { - selection++; - if (selection >= options.length()) selection = 0; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_RETURN)) { - if (selection == 0) craft_knife(); - else if (selection == 1) craft_snare(); - else if (selection == 2) craft_axe(); - else if (selection == 3) craft_fishing_pole(); - else if (selection == 4) butcher_small_game(); - break; - } - } -} - -void run_buildings_menu() { - screen_reader_speak("Buildings.", true); - - int selection = 0; - string[] options = { - "Firepit (9 Stones)", - "Fire (2 Sticks, 1 Log) [Requires Firepit]", - "Herb Garden (9 Stones, 3 Vines, 2 Logs) [Base Only]" - }; - - while(true) { - wait(5); - menu_background_tick(); - if (key_pressed(KEY_ESCAPE)) { - screen_reader_speak("Closed.", true); - break; - } - - if (key_pressed(KEY_DOWN)) { - selection++; - if (selection >= options.length()) selection = 0; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_RETURN)) { - if (selection == 0) craft_firepit(); - else if (selection == 1) craft_campfire(); - else if (selection == 2) craft_herb_garden(); - break; - } - } -} - -void run_barricade_menu() { - if (barricade_health >= BARRICADE_MAX_HEALTH) { - screen_reader_speak("Barricade is already at full health.", true); - return; - } - - screen_reader_speak("Barricade.", true); - - int selection = 0; - string[] options; - int[] action_types; // 0 = sticks, 1 = vines, 2 = log, 3 = stones - - if (inv_sticks >= BARRICADE_STICK_COST) { - options.insert_last("Reinforce with sticks (" + BARRICADE_STICK_COST + " sticks, +" + BARRICADE_STICK_HEALTH + " health)"); - action_types.insert_last(0); - } - if (inv_vines >= BARRICADE_VINE_COST) { - options.insert_last("Reinforce with vines (" + BARRICADE_VINE_COST + " vines, +" + BARRICADE_VINE_HEALTH + " health)"); - action_types.insert_last(1); - } - if (inv_logs >= BARRICADE_LOG_COST) { - options.insert_last("Reinforce with log (" + BARRICADE_LOG_COST + " log, +" + BARRICADE_LOG_HEALTH + " health)"); - action_types.insert_last(2); - } - if (inv_stones >= BARRICADE_STONE_COST) { - options.insert_last("Reinforce with stones (" + BARRICADE_STONE_COST + " stones, +" + BARRICADE_STONE_HEALTH + " health)"); - action_types.insert_last(3); - } - - if (options.length() == 0) { - screen_reader_speak("No materials to reinforce the barricade.", true); - return; - } - - while(true) { - wait(5); - menu_background_tick(); - if (key_pressed(KEY_ESCAPE)) { - screen_reader_speak("Closed.", true); - break; - } - - if (key_pressed(KEY_DOWN)) { - selection++; - if (selection >= options.length()) selection = 0; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_RETURN)) { - int action = action_types[selection]; - if (action == 0) reinforce_barricade_with_sticks(); - else if (action == 1) reinforce_barricade_with_vines(); - else if (action == 2) reinforce_barricade_with_log(); - else if (action == 3) reinforce_barricade_with_stones(); - break; - } - } -} - -void run_equipment_menu() { - screen_reader_speak("Equipment menu.", true); - - int selection = 0; - string[] options; - int[] equipment_types; // 0 = spear, 1 = axe, 2 = sling - - // Build menu dynamically based on what player has - if (inv_spears > 0) { - string status = spear_equipped ? " (equipped)" : ""; - options.insert_last("Spear" + status); - equipment_types.insert_last(0); - } - if (inv_slings > 0) { - string status = sling_equipped ? " (equipped)" : ""; - options.insert_last("Sling" + status); - equipment_types.insert_last(2); - } - if (inv_axes > 0) { - string status = axe_equipped ? " (equipped)" : ""; - options.insert_last("Stone Axe" + status); - equipment_types.insert_last(1); - } - - while(true) { - wait(5); - menu_background_tick(); - if (key_pressed(KEY_ESCAPE)) { - screen_reader_speak("Closed.", true); - break; - } - - if (key_pressed(KEY_DOWN)) { - selection++; - if (selection >= options.length()) selection = 0; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_UP)) { - selection--; - if (selection < 0) selection = options.length() - 1; - screen_reader_speak(options[selection], true); - } - - if (key_pressed(KEY_RETURN)) { - int equip_type = equipment_types[selection]; - - if (equip_type == 0) { - // Spear - if (!spear_equipped) { - spear_equipped = true; - axe_equipped = false; - sling_equipped = false; - screen_reader_speak("Spear equipped.", true); - } else { - spear_equipped = false; - screen_reader_speak("Spear unequipped.", true); - } - } else if (equip_type == 1) { - // Axe - if (!axe_equipped) { - axe_equipped = true; - spear_equipped = false; - sling_equipped = false; - screen_reader_speak("Stone Axe equipped.", true); - } else { - axe_equipped = false; - screen_reader_speak("Stone Axe unequipped.", true); - } - } else if (equip_type == 2) { - // Sling - if (!sling_equipped) { - sling_equipped = true; - spear_equipped = false; - axe_equipped = false; - screen_reader_speak("Sling equipped.", true); - } else { - sling_equipped = false; - screen_reader_speak("Sling unequipped.", true); - } - } - break; - } - } -} - -void simulate_crafting() { - screen_reader_speak("Crafting...", true); - timer t; - int duration = 4000; - int next_sound = 0; - - while(t.elapsed < duration) { - if(t.elapsed > next_sound) { - float pitch = random(85, 115); - p.play_stationary_extended("sounds/crafting.ogg", false, 0, 0, 0, pitch); - next_sound = t.elapsed + 800; - } - wait(5); - menu_background_tick(); - } - p.play_stationary("sounds/crafting_complete.ogg", false); -} - -void craft_knife() { - string missing = ""; - if (inv_stones < 2) missing += "2 stones "; - - if (missing == "") { - if (inv_knives >= MAX_ITEM_STACK) { - screen_reader_speak("You can't carry any more stone knives.", true); - return; - } - simulate_crafting(); - inv_stones -= 2; - inv_knives++; - screen_reader_speak("Crafted a Stone Knife.", true); - } else { - screen_reader_speak("Missing: " + missing, true); - } -} - -void craft_spear() { - string missing = ""; - if (inv_knives < 1) missing += "Stone Knife "; - if (inv_sticks < 1) missing += "1 stick "; - if (inv_vines < 1) missing += "1 vine "; - if (inv_stones < 1) missing += "1 stone "; - - if (missing == "") { - if (inv_spears >= MAX_ITEM_STACK) { - screen_reader_speak("You can't carry any more spears.", true); - return; - } - simulate_crafting(); - inv_sticks--; - inv_vines--; - inv_stones--; - inv_spears++; - screen_reader_speak("Crafted a Spear.", true); - } else { - screen_reader_speak("Missing: " + missing, true); - } -} - -void craft_sling() { - string missing = ""; - if (inv_skins < 1) missing += "1 skin "; - if (inv_vines < 2) missing += "2 vines "; - - if (missing == "") { - if (inv_slings >= MAX_ITEM_STACK) { - screen_reader_speak("You can't carry any more slings.", true); - return; - } - simulate_crafting(); - inv_skins--; - inv_vines -= 2; - inv_slings++; - screen_reader_speak("Crafted a Sling.", true); - } else { - screen_reader_speak("Missing: " + missing, true); - } -} - -void craft_snare() { - string missing = ""; - if (inv_sticks < 1) missing += "1 stick "; - if (inv_vines < 2) missing += "2 vines "; - - if (missing == "") { - if (inv_snares >= MAX_ITEM_STACK) { - screen_reader_speak("You can't carry any more snares.", true); - return; - } - simulate_crafting(); - inv_sticks--; - inv_vines -= 2; - inv_snares++; - screen_reader_speak("Crafted a Snare.", true); - } else { - screen_reader_speak("Missing: " + missing, true); - } -} - -void craft_axe() { - string missing = ""; - if (inv_knives < 1) missing += "Stone Knife "; - if (inv_sticks < 1) missing += "1 stick "; - if (inv_vines < 1) missing += "1 vine "; - if (inv_stones < 2) missing += "2 stones "; - - if (missing == "") { - if (inv_axes >= MAX_ITEM_STACK) { - screen_reader_speak("You can't carry any more stone axes.", true); - return; - } - simulate_crafting(); - inv_sticks--; - inv_vines--; - inv_stones -= 2; - inv_axes++; - screen_reader_speak("Crafted a Stone Axe.", true); - } else { - screen_reader_speak("Missing: " + missing, true); - } -} - -void craft_firepit() { - // Check if there's already a firepit here - if (get_firepit_at(x) != null) { - screen_reader_speak("There is already a firepit here.", true); - return; - } - - string missing = ""; - if (inv_stones < 9) missing += "9 stones "; - - if (missing == "") { - simulate_crafting(); - inv_stones -= 9; - add_world_firepit(x); - screen_reader_speak("Firepit built here.", true); - } else { - screen_reader_speak("Missing: " + missing, true); - } -} - -void craft_campfire() { - // Check if there's a firepit within 2 tiles - WorldFirepit@ firepit = get_firepit_near(x, 2); - if (firepit == null) { - screen_reader_speak("You need a firepit within 2 tiles to build a fire.", true); - return; - } - - string missing = ""; - if (inv_logs < 1) missing += "1 log "; - if (inv_sticks < 2) missing += "2 sticks "; - - if (missing == "") { - simulate_crafting(); - inv_logs--; - inv_sticks -= 2; - // Build the fire at the firepit location, not player location - add_world_fire(firepit.position); - screen_reader_speak("Fire built at firepit.", true); - } else { - screen_reader_speak("Missing: " + missing, true); - } -} - -void craft_herb_garden() { - // Can only build in base area - if (x > BASE_END) { - screen_reader_speak("Herb garden can only be built in the base area.", true); - return; - } - - // Check if there's already an herb garden in the base - if (get_herb_garden_at_base() != null) { - screen_reader_speak("There is already an herb garden in the base.", true); - return; - } - - string missing = ""; - if (inv_stones < 9) missing += "9 stones "; - if (inv_vines < 3) missing += "3 vines "; - if (inv_logs < 2) missing += "2 logs "; - - if (missing == "") { - simulate_crafting(); - inv_stones -= 9; - inv_vines -= 3; - inv_logs -= 2; - add_world_herb_garden(x); - screen_reader_speak("Herb garden built. The base now heals faster.", true); - } else { - screen_reader_speak("Missing: " + missing, true); - } -} - -void reinforce_barricade_with_sticks() { - if (barricade_health >= BARRICADE_MAX_HEALTH) { - screen_reader_speak("Barricade is already at full health.", true); - return; - } - if (inv_sticks < BARRICADE_STICK_COST) { - screen_reader_speak("Not enough sticks.", true); - return; - } - - simulate_crafting(); - inv_sticks -= BARRICADE_STICK_COST; - int gained = add_barricade_health(BARRICADE_STICK_HEALTH); - screen_reader_speak("Reinforced barricade with sticks. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); -} - -void reinforce_barricade_with_vines() { - if (barricade_health >= BARRICADE_MAX_HEALTH) { - screen_reader_speak("Barricade is already at full health.", true); - return; - } - if (inv_vines < BARRICADE_VINE_COST) { - screen_reader_speak("Not enough vines.", true); - return; - } - - simulate_crafting(); - inv_vines -= BARRICADE_VINE_COST; - int gained = add_barricade_health(BARRICADE_VINE_HEALTH); - screen_reader_speak("Reinforced barricade with vines. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); -} - -void reinforce_barricade_with_log() { - if (barricade_health >= BARRICADE_MAX_HEALTH) { - screen_reader_speak("Barricade is already at full health.", true); - return; - } - if (inv_logs < BARRICADE_LOG_COST) { - screen_reader_speak("Not enough logs.", true); - return; - } - - simulate_crafting(); - inv_logs -= BARRICADE_LOG_COST; - int gained = add_barricade_health(BARRICADE_LOG_HEALTH); - screen_reader_speak("Reinforced barricade with log. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); -} - -void reinforce_barricade_with_stones() { - if (barricade_health >= BARRICADE_MAX_HEALTH) { - screen_reader_speak("Barricade is already at full health.", true); - return; - } - if (inv_stones < BARRICADE_STONE_COST) { - screen_reader_speak("Not enough stones.", true); - return; - } - - simulate_crafting(); - inv_stones -= BARRICADE_STONE_COST; - int gained = add_barricade_health(BARRICADE_STONE_HEALTH); - screen_reader_speak("Reinforced barricade with stones. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); -} - -void craft_fishing_pole() { - string missing = ""; - if (inv_sticks < 1) missing += "1 stick "; - if (inv_vines < 2) missing += "2 vines "; - - if (missing == "") { - if (inv_fishing_poles >= MAX_ITEM_STACK) { - screen_reader_speak("You can't carry any more fishing poles.", true); - return; - } - simulate_crafting(); - inv_sticks--; - inv_vines -= 2; - inv_fishing_poles++; - screen_reader_speak("Crafted a Fishing Pole.", true); - } else { - screen_reader_speak("Missing: " + missing, true); - } -} - -void butcher_small_game() { - string missing = ""; - - // Check for knife - if (inv_knives < 1) missing += "Stone Knife "; - - // Check for small game - if (inv_small_game < 1) missing += "Small Game "; - - // Check for fire within 3 tiles (can hear it) - WorldFire@ fire = get_fire_within_range(x, 3); - if (fire == null) { - screen_reader_speak("You need a fire within 3 tiles to butcher.", true); - return; - } - - if (missing == "") { - if (inv_meat >= MAX_ITEM_STACK) { - screen_reader_speak("You can't carry any more meat.", true); - return; - } - if (inv_skins >= MAX_ITEM_STACK) { - screen_reader_speak("You can't carry any more skins.", true); - return; - } - simulate_crafting(); - - // Get the type of game we're butchering (first in the list) - string game_type = inv_small_game_types[0]; - inv_small_game_types.remove_at(0); - - inv_small_game--; - inv_meat++; - inv_skins++; - - screen_reader_speak("Butchered " + game_type + ". Got 1 meat and 1 skin.", true); - } else { - screen_reader_speak("Missing: " + missing, true); - } -} +// Inventory module includes +#include "src/inventory_items.nvgt" +#include "src/inventory_menus.nvgt" +#include "src/crafting.nvgt" diff --git a/src/inventory_items.nvgt b/src/inventory_items.nvgt new file mode 100644 index 0000000..f451bab --- /dev/null +++ b/src/inventory_items.nvgt @@ -0,0 +1,439 @@ +// Inventory items and equipment +int inv_stones = 0; +int inv_sticks = 0; +int inv_vines = 0; +int inv_reeds = 0; +int inv_logs = 0; +int inv_clay = 0; +int inv_small_game = 0; // Total small game caught (any type) +string[] inv_small_game_types; // Array to track what types of small game we have + +int inv_meat = 0; +int inv_skins = 0; + +int inv_spears = 0; +int inv_snares = 0; +int inv_axes = 0; +int inv_knives = 0; +int inv_fishing_poles = 0; +int inv_slings = 0; +int inv_ropes = 0; +int inv_reed_baskets = 0; +int inv_clay_pots = 0; +int inv_skin_hats = 0; +int inv_skin_gloves = 0; +int inv_skin_pants = 0; +int inv_skin_tunics = 0; +int inv_moccasins = 0; +int inv_skin_pouches = 0; + +int storage_stones = 0; +int storage_sticks = 0; +int storage_vines = 0; +int storage_reeds = 0; +int storage_logs = 0; +int storage_clay = 0; +int storage_small_game = 0; +string[] storage_small_game_types; +int storage_meat = 0; +int storage_skins = 0; +int storage_spears = 0; +int storage_snares = 0; +int storage_axes = 0; +int storage_knives = 0; +int storage_fishing_poles = 0; +int storage_slings = 0; +int storage_ropes = 0; +int storage_reed_baskets = 0; +int storage_clay_pots = 0; +int storage_skin_hats = 0; +int storage_skin_gloves = 0; +int storage_skin_pants = 0; +int storage_skin_tunics = 0; +int storage_moccasins = 0; +int storage_skin_pouches = 0; + +bool spear_equipped = false; +bool axe_equipped = false; +bool sling_equipped = false; +int[] quick_slots; +const int EQUIP_NONE = -1; +const int EQUIP_SPEAR = 0; +const int EQUIP_AXE = 1; +const int EQUIP_SLING = 2; +const int EQUIP_HAT = 3; +const int EQUIP_GLOVES = 4; +const int EQUIP_PANTS = 5; +const int EQUIP_TUNIC = 6; +const int EQUIP_MOCCASINS = 7; +const int EQUIP_POUCH = 8; +const int ITEM_STICKS = 0; +const int ITEM_VINES = 1; +const int ITEM_REEDS = 2; +const int ITEM_STONES = 3; +const int ITEM_LOGS = 4; +const int ITEM_CLAY = 5; +const int ITEM_SMALL_GAME = 6; +const int ITEM_MEAT = 7; +const int ITEM_SKINS = 8; +const int ITEM_SPEARS = 9; +const int ITEM_SLINGS = 10; +const int ITEM_AXES = 11; +const int ITEM_SNARES = 12; +const int ITEM_KNIVES = 13; +const int ITEM_FISHING_POLES = 14; +const int ITEM_SKIN_HATS = 15; +const int ITEM_SKIN_GLOVES = 16; +const int ITEM_SKIN_PANTS = 17; +const int ITEM_SKIN_TUNICS = 18; +const int ITEM_MOCCASINS = 19; +const int ITEM_SKIN_POUCHES = 20; +const int ITEM_ROPES = 21; +const int ITEM_REED_BASKETS = 22; +const int ITEM_CLAY_POTS = 23; +const int HAT_MAX_HEALTH_BONUS = 1; +const int GLOVES_MAX_HEALTH_BONUS = 1; +const int PANTS_MAX_HEALTH_BONUS = 3; +const int TUNIC_MAX_HEALTH_BONUS = 4; +const int MOCCASINS_MAX_HEALTH_BONUS = 2; +int equipped_head = EQUIP_NONE; +int equipped_torso = EQUIP_NONE; +int equipped_arms = EQUIP_NONE; +int equipped_hands = EQUIP_NONE; +int equipped_legs = EQUIP_NONE; +int equipped_feet = EQUIP_NONE; + +void reset_quick_slots() { + quick_slots.resize(10); + for (uint i = 0; i < quick_slots.length(); i++) { + quick_slots[i] = -1; + } +} + +int get_personal_stack_limit() { + int limit = MAX_ITEM_STACK; + if (equipped_arms == EQUIP_POUCH) { + limit += POUCH_STACK_BONUS; + } + return limit; +} + +string get_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_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"; + return "Unknown"; +} + +bool equipment_available(int equip_type) { + if (equip_type == EQUIP_SPEAR) return inv_spears > 0; + if (equip_type == EQUIP_AXE) return inv_axes > 0; + if (equip_type == EQUIP_SLING) return inv_slings > 0; + if (equip_type == EQUIP_HAT) return inv_skin_hats > 0; + if (equip_type == EQUIP_GLOVES) return inv_skin_gloves > 0; + if (equip_type == EQUIP_PANTS) return inv_skin_pants > 0; + if (equip_type == EQUIP_TUNIC) return inv_skin_tunics > 0; + if (equip_type == EQUIP_MOCCASINS) return inv_moccasins > 0; + if (equip_type == EQUIP_POUCH) return inv_skin_pouches > 0; + return false; +} + +void equip_equipment_type(int equip_type) { + if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || equip_type == EQUIP_SLING) { + spear_equipped = (equip_type == EQUIP_SPEAR); + axe_equipped = (equip_type == EQUIP_AXE); + sling_equipped = (equip_type == EQUIP_SLING); + return; + } + + if (equip_type == EQUIP_HAT) equipped_head = EQUIP_HAT; + else if (equip_type == EQUIP_TUNIC) equipped_torso = EQUIP_TUNIC; + else if (equip_type == EQUIP_GLOVES) equipped_hands = EQUIP_GLOVES; + else if (equip_type == EQUIP_PANTS) equipped_legs = EQUIP_PANTS; + else if (equip_type == EQUIP_MOCCASINS) equipped_feet = EQUIP_MOCCASINS; + else if (equip_type == EQUIP_POUCH) equipped_arms = EQUIP_POUCH; +} + +bool equipment_is_equipped(int equip_type) { + if (equip_type == EQUIP_SPEAR) return spear_equipped; + if (equip_type == EQUIP_AXE) return axe_equipped; + if (equip_type == EQUIP_SLING) return sling_equipped; + if (equip_type == EQUIP_HAT) return equipped_head == EQUIP_HAT; + if (equip_type == EQUIP_TUNIC) return equipped_torso == EQUIP_TUNIC; + if (equip_type == EQUIP_GLOVES) return equipped_hands == EQUIP_GLOVES; + if (equip_type == EQUIP_PANTS) return equipped_legs == EQUIP_PANTS; + if (equip_type == EQUIP_MOCCASINS) return equipped_feet == EQUIP_MOCCASINS; + if (equip_type == EQUIP_POUCH) return equipped_arms == EQUIP_POUCH; + return false; +} + +void unequip_equipment_type(int equip_type) { + if (equip_type == EQUIP_SPEAR) { + spear_equipped = false; + } else if (equip_type == EQUIP_AXE) { + axe_equipped = false; + } else if (equip_type == EQUIP_SLING) { + sling_equipped = false; + } else if (equip_type == EQUIP_HAT && equipped_head == EQUIP_HAT) { + equipped_head = EQUIP_NONE; + } else if (equip_type == EQUIP_TUNIC && equipped_torso == EQUIP_TUNIC) { + equipped_torso = EQUIP_NONE; + } else if (equip_type == EQUIP_GLOVES && equipped_hands == EQUIP_GLOVES) { + equipped_hands = EQUIP_NONE; + } else if (equip_type == EQUIP_PANTS && equipped_legs == EQUIP_PANTS) { + equipped_legs = EQUIP_NONE; + } else if (equip_type == EQUIP_MOCCASINS && equipped_feet == EQUIP_MOCCASINS) { + equipped_feet = EQUIP_NONE; + } else if (equip_type == EQUIP_POUCH && equipped_arms == EQUIP_POUCH) { + equipped_arms = EQUIP_NONE; + } +} + +void update_max_health_from_equipment() { + int bonus = 0; + if (equipped_head == EQUIP_HAT) bonus += HAT_MAX_HEALTH_BONUS; + if (equipped_hands == EQUIP_GLOVES) bonus += GLOVES_MAX_HEALTH_BONUS; + if (equipped_legs == EQUIP_PANTS) bonus += PANTS_MAX_HEALTH_BONUS; + if (equipped_torso == EQUIP_TUNIC) bonus += TUNIC_MAX_HEALTH_BONUS; + if (equipped_feet == EQUIP_MOCCASINS) bonus += MOCCASINS_MAX_HEALTH_BONUS; + max_health = base_max_health + bonus; + if (player_health > max_health) { + player_health = max_health; + } + int base_speed = (equipped_feet == EQUIP_MOCCASINS) ? MOCCASINS_WALK_SPEED : BASE_WALK_SPEED; + walk_speed = base_speed; + if (blessing_speed_active && BLESSING_WALK_SPEED < walk_speed) { + walk_speed = BLESSING_WALK_SPEED; + } +} + +int get_quick_slot_key() { + if (key_pressed(KEY_1)) return 1; + if (key_pressed(KEY_2)) return 2; + if (key_pressed(KEY_3)) return 3; + if (key_pressed(KEY_4)) return 4; + if (key_pressed(KEY_5)) return 5; + if (key_pressed(KEY_6)) return 6; + if (key_pressed(KEY_7)) return 7; + if (key_pressed(KEY_8)) return 8; + if (key_pressed(KEY_9)) return 9; + if (key_pressed(KEY_0)) return 0; + return -1; +} + +void activate_quick_slot(int slot_index) { + if (slot_index < 0 || slot_index >= int(quick_slots.length())) { + return; + } + + int equip_type = quick_slots[slot_index]; + if (equip_type < 0) { + screen_reader_speak("No item bound to slot " + slot_index + ".", true); + return; + } + + if (!equipment_available(equip_type)) { + screen_reader_speak("Item not available.", true); + return; + } + + equip_equipment_type(equip_type); + update_max_health_from_equipment(); + screen_reader_speak(get_equipment_name(equip_type) + " equipped.", true); +} + +void check_quick_slot_keys() { + int slot_index = get_quick_slot_key(); + if (slot_index != -1) { + activate_quick_slot(slot_index); + } +} + +int add_to_stack(int current, int amount) { + if (amount <= 0) return 0; + int space = get_personal_stack_limit() - current; + if (space <= 0) return 0; + if (amount > space) return space; + return amount; +} + +int get_personal_count(int item_type) { + if (item_type == ITEM_STICKS) return inv_sticks; + if (item_type == ITEM_VINES) return inv_vines; + if (item_type == ITEM_REEDS) return inv_reeds; + if (item_type == ITEM_STONES) return inv_stones; + if (item_type == ITEM_LOGS) return inv_logs; + if (item_type == ITEM_CLAY) return inv_clay; + if (item_type == ITEM_SMALL_GAME) return inv_small_game; + if (item_type == ITEM_MEAT) return inv_meat; + if (item_type == ITEM_SKINS) return inv_skins; + if (item_type == ITEM_SPEARS) return inv_spears; + if (item_type == ITEM_SLINGS) return inv_slings; + if (item_type == ITEM_AXES) return inv_axes; + if (item_type == ITEM_SNARES) return inv_snares; + if (item_type == ITEM_KNIVES) return inv_knives; + if (item_type == ITEM_FISHING_POLES) return inv_fishing_poles; + if (item_type == ITEM_ROPES) return inv_ropes; + if (item_type == ITEM_REED_BASKETS) return inv_reed_baskets; + if (item_type == ITEM_CLAY_POTS) return inv_clay_pots; + if (item_type == ITEM_SKIN_HATS) return inv_skin_hats; + if (item_type == ITEM_SKIN_GLOVES) return inv_skin_gloves; + if (item_type == ITEM_SKIN_PANTS) return inv_skin_pants; + if (item_type == ITEM_SKIN_TUNICS) return inv_skin_tunics; + if (item_type == ITEM_MOCCASINS) return inv_moccasins; + if (item_type == ITEM_SKIN_POUCHES) return inv_skin_pouches; + return 0; +} + +int get_storage_count(int item_type) { + if (item_type == ITEM_STICKS) return storage_sticks; + if (item_type == ITEM_VINES) return storage_vines; + if (item_type == ITEM_REEDS) return storage_reeds; + if (item_type == ITEM_STONES) return storage_stones; + if (item_type == ITEM_LOGS) return storage_logs; + if (item_type == ITEM_CLAY) return storage_clay; + if (item_type == ITEM_SMALL_GAME) return storage_small_game; + if (item_type == ITEM_MEAT) return storage_meat; + if (item_type == ITEM_SKINS) return storage_skins; + if (item_type == ITEM_SPEARS) return storage_spears; + if (item_type == ITEM_SLINGS) return storage_slings; + if (item_type == ITEM_AXES) return storage_axes; + if (item_type == ITEM_SNARES) return storage_snares; + if (item_type == ITEM_KNIVES) return storage_knives; + if (item_type == ITEM_FISHING_POLES) return storage_fishing_poles; + if (item_type == ITEM_ROPES) return storage_ropes; + if (item_type == ITEM_REED_BASKETS) return storage_reed_baskets; + if (item_type == ITEM_CLAY_POTS) return storage_clay_pots; + if (item_type == ITEM_SKIN_HATS) return storage_skin_hats; + if (item_type == ITEM_SKIN_GLOVES) return storage_skin_gloves; + if (item_type == ITEM_SKIN_PANTS) return storage_skin_pants; + if (item_type == ITEM_SKIN_TUNICS) return storage_skin_tunics; + if (item_type == ITEM_MOCCASINS) return storage_moccasins; + if (item_type == ITEM_SKIN_POUCHES) return storage_skin_pouches; + return 0; +} + +string get_item_label(int item_type) { + if (item_type == ITEM_STICKS) return "sticks"; + if (item_type == ITEM_VINES) return "vines"; + if (item_type == ITEM_REEDS) return "reeds"; + if (item_type == ITEM_STONES) return "stones"; + if (item_type == ITEM_LOGS) return "logs"; + if (item_type == ITEM_CLAY) return "clay"; + if (item_type == ITEM_SMALL_GAME) return "small game"; + if (item_type == ITEM_MEAT) return "meat"; + if (item_type == ITEM_SKINS) return "skins"; + if (item_type == ITEM_SPEARS) return "spears"; + if (item_type == ITEM_SLINGS) return "slings"; + if (item_type == ITEM_AXES) return "axes"; + if (item_type == ITEM_SNARES) return "snares"; + if (item_type == ITEM_KNIVES) return "knives"; + if (item_type == ITEM_FISHING_POLES) return "fishing poles"; + if (item_type == ITEM_ROPES) return "ropes"; + if (item_type == ITEM_REED_BASKETS) return "reed baskets"; + if (item_type == ITEM_CLAY_POTS) return "clay pots"; + if (item_type == ITEM_SKIN_HATS) return "skin hats"; + if (item_type == ITEM_SKIN_GLOVES) return "skin gloves"; + if (item_type == ITEM_SKIN_PANTS) return "skin pants"; + if (item_type == ITEM_SKIN_TUNICS) return "skin tunics"; + if (item_type == ITEM_MOCCASINS) return "moccasins"; + if (item_type == ITEM_SKIN_POUCHES) return "skin pouches"; + return "items"; +} + +string format_favor(double value) { + if (value < 0) value = 0; + int scaled = int(value * 100 + 0.5); + int whole = scaled / 100; + int frac = scaled % 100; + string frac_text = (frac < 10) ? "0" + frac : "" + frac; + return "" + whole + "." + frac_text; +} + +string get_item_label_singular(int item_type) { + if (item_type == ITEM_STICKS) return "stick"; + if (item_type == ITEM_VINES) return "vine"; + if (item_type == ITEM_REEDS) return "reed"; + if (item_type == ITEM_STONES) return "stone"; + if (item_type == ITEM_LOGS) return "log"; + if (item_type == ITEM_CLAY) return "clay"; + if (item_type == ITEM_SMALL_GAME) return "small game"; + if (item_type == ITEM_MEAT) return "meat"; + if (item_type == ITEM_SKINS) return "skin"; + if (item_type == ITEM_SPEARS) return "spear"; + if (item_type == ITEM_SLINGS) return "sling"; + if (item_type == ITEM_AXES) return "axe"; + if (item_type == ITEM_SNARES) return "snare"; + if (item_type == ITEM_KNIVES) return "knife"; + if (item_type == ITEM_FISHING_POLES) return "fishing pole"; + if (item_type == ITEM_ROPES) return "rope"; + if (item_type == ITEM_REED_BASKETS) return "reed basket"; + if (item_type == ITEM_CLAY_POTS) return "clay pot"; + if (item_type == ITEM_SKIN_HATS) return "skin hat"; + if (item_type == ITEM_SKIN_GLOVES) return "skin glove"; + if (item_type == ITEM_SKIN_PANTS) return "skin pants"; + if (item_type == ITEM_SKIN_TUNICS) return "skin tunic"; + if (item_type == ITEM_MOCCASINS) return "moccasin"; + if (item_type == ITEM_SKIN_POUCHES) return "skin pouch"; + return "item"; +} + +double get_item_favor_value(int item_type) { + if (item_type == ITEM_STICKS) return 0.01; + if (item_type == ITEM_VINES) return 0.01; + if (item_type == ITEM_REEDS) return 0.01; + if (item_type == ITEM_STONES) return 0.02; + if (item_type == ITEM_LOGS) return 0.05; + if (item_type == ITEM_CLAY) return 0.02; + if (item_type == ITEM_SMALL_GAME) return 0.20; + if (item_type == ITEM_MEAT) return 0.15; + if (item_type == ITEM_SKINS) return 0.15; + if (item_type == ITEM_SPEARS) return 1.00; + if (item_type == ITEM_SLINGS) return 2.00; + if (item_type == ITEM_AXES) return 1.50; + if (item_type == ITEM_SNARES) return 0.50; + if (item_type == ITEM_KNIVES) return 0.80; + if (item_type == ITEM_FISHING_POLES) return 0.80; + if (item_type == ITEM_ROPES) return 0.40; + if (item_type == ITEM_REED_BASKETS) return 0.60; + if (item_type == ITEM_CLAY_POTS) return 0.70; + if (item_type == ITEM_SKIN_HATS) return 0.60; + if (item_type == ITEM_SKIN_GLOVES) return 0.60; + if (item_type == ITEM_SKIN_PANTS) return 1.20; + if (item_type == ITEM_SKIN_TUNICS) return 1.20; + if (item_type == ITEM_MOCCASINS) return 0.80; + if (item_type == ITEM_SKIN_POUCHES) return 0.80; + return 0.01; +} + +string get_equipped_weapon_name() { + if (spear_equipped) return "Spear"; + if (axe_equipped) return "Stone Axe"; + if (sling_equipped) return "Sling"; + return "None"; +} + +string get_speed_status() { + if (blessing_speed_active) return "blessed"; + if (equipped_feet == EQUIP_MOCCASINS) return "boosted by moccasins"; + return "normal"; +} + +void cleanup_equipment_after_inventory_change() { + if (inv_spears <= 0) spear_equipped = false; + if (inv_axes <= 0) axe_equipped = false; + if (inv_slings <= 0) sling_equipped = false; + if (inv_skin_hats <= 0) equipped_head = EQUIP_NONE; + if (inv_skin_gloves <= 0) equipped_hands = EQUIP_NONE; + if (inv_skin_pants <= 0) equipped_legs = EQUIP_NONE; + if (inv_skin_tunics <= 0) equipped_torso = EQUIP_NONE; + if (inv_moccasins <= 0) equipped_feet = EQUIP_NONE; + if (inv_skin_pouches <= 0) equipped_arms = EQUIP_NONE; + update_max_health_from_equipment(); +} diff --git a/src/inventory_menus.nvgt b/src/inventory_menus.nvgt new file mode 100644 index 0000000..3dd1664 --- /dev/null +++ b/src/inventory_menus.nvgt @@ -0,0 +1,811 @@ +// Inventory and base menus +void check_inventory_keys(int x) { + if (key_pressed(KEY_P)) { + show_character_info(); + return; + } + if (key_pressed(KEY_I)) { + bool in_base = x <= BASE_END; + if (in_base && world_storages.length() > 0) { + run_inventory_root_menu(); + } else { + if (in_base && world_storages.length() == 0) { + screen_reader_speak("No storage built.", true); + } + run_inventory_menu(false); + } + } +} + +void check_action_menu(int x) { + if (key_pressed(KEY_A)) { + run_action_menu(x); + } +} + +void menu_background_tick() { + update_time(); + update_environment(); + update_snares(); + update_fires(); + update_zombies(); + update_blessings(); + update_notifications(); + + // Fire damage check (only if not jumping) + WorldFire@ fire_on_tile = get_fire_at(x); + if (fire_on_tile != null && !jumping && fire_damage_timer.elapsed > 1000) { + player_health--; + fire_damage_timer.restart(); + screen_reader_speak("Burning! " + player_health + " health remaining.", true); + } + + // Healing in base area + if (x <= BASE_END && player_health < max_health) { + WorldHerbGarden@ herb_garden = get_herb_garden_at_base(); + int heal_interval = (herb_garden != null) ? 30000 : 150000; // 30 seconds with garden, 2.5 minutes without + + if (healing_timer.elapsed > heal_interval) { + player_health++; + healing_timer.restart(); + screen_reader_speak(player_health + " health.", true); + } + } + + // Death check + if (player_health <= 0) { + screen_reader_speak("You have died.", true); + wait(2000); + exit(); + } +} + +int get_storage_total_items() { + int total = 0; + total += storage_stones; + total += storage_sticks; + total += storage_vines; + total += storage_reeds; + total += storage_logs; + total += storage_clay; + total += storage_small_game; + total += storage_meat; + total += storage_skins; + total += storage_spears; + total += storage_slings; + total += storage_axes; + total += storage_snares; + total += storage_knives; + total += storage_fishing_poles; + total += storage_ropes; + total += storage_reed_baskets; + total += storage_clay_pots; + total += storage_skin_hats; + total += storage_skin_gloves; + total += storage_skin_pants; + total += storage_skin_tunics; + total += storage_moccasins; + total += storage_skin_pouches; + return total; +} + +string get_base_fire_status() { + int total = 0; + int burning = 0; + for (uint i = 0; i < world_fires.length(); i++) { + if (world_fires[i].position <= BASE_END) { + total++; + if (world_fires[i].is_burning()) { + burning++; + } + } + } + if (total == 0) return "No fires in base"; + return "Fires in base: " + burning + " burning, " + total + " total"; +} + +void run_base_info_menu() { + if (x > BASE_END) { + screen_reader_speak("You are not in the base.", true); + return; + } + + screen_reader_speak("Base info.", true); + + int selection = 0; + string[] options; + options.insert_last("Barricade health " + barricade_health + " of " + BARRICADE_MAX_HEALTH); + options.insert_last("Residents " + residents_count); + + if (world_storages.length() > 0) { + options.insert_last("Storage built. Total items " + get_storage_total_items()); + int daily_food = get_daily_food_requirement(); + options.insert_last("Food in storage " + storage_meat + " meat. Daily use " + daily_food); + } else { + options.insert_last("Storage not built"); + } + + options.insert_last(get_base_fire_status()); + + if (world_altars.length() > 0) options.insert_last("Altar built"); + else options.insert_last("Altar not built"); + + if (get_herb_garden_at_base() != null) options.insert_last("Herb garden built"); + else options.insert_last("Herb garden not built"); + + if (world_pastures.length() > 0) options.insert_last("Pasture built"); + else options.insert_last("Pasture not built"); + + if (world_stables.length() > 0) options.insert_last("Stable built"); + else options.insert_last("Stable not built"); + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], true); + } + } +} + +string join_string_list(const string[]@ items) { + if (@items == null || items.length() == 0) return ""; + string result = items[0]; + for (uint i = 1; i < items.length(); i++) { + result += ", " + items[i]; + } + return result; +} + +void show_character_info() { + string[] equipped_clothing; + string[] missing_slots; + + if (equipped_head == EQUIP_HAT) equipped_clothing.insert_last("Skin Hat"); + else missing_slots.insert_last("head"); + if (equipped_torso == EQUIP_TUNIC) equipped_clothing.insert_last("Skin Tunic"); + else missing_slots.insert_last("torso"); + if (equipped_arms == EQUIP_POUCH) equipped_clothing.insert_last("Skin Pouch"); + else missing_slots.insert_last("arms"); + if (equipped_hands == EQUIP_GLOVES) equipped_clothing.insert_last("Skin Gloves"); + else missing_slots.insert_last("hands"); + if (equipped_legs == EQUIP_PANTS) equipped_clothing.insert_last("Skin Pants"); + else missing_slots.insert_last("legs"); + if (equipped_feet == EQUIP_MOCCASINS) equipped_clothing.insert_last("Moccasins"); + else missing_slots.insert_last("feet"); + + string info = "Character info. "; + info += "Health " + player_health + " of " + max_health + ". "; + info += "Weapon " + get_equipped_weapon_name() + ". "; + if (equipped_clothing.length() > 0) { + info += "Clothing equipped: " + join_string_list(equipped_clothing) + ". "; + } else { + info += "No clothing equipped. "; + } + if (missing_slots.length() > 0) { + info += "Missing " + join_string_list(missing_slots) + ". "; + } + info += "Favor " + format_favor(favor) + ". "; + info += "Speed " + get_speed_status() + "."; + screen_reader_speak(info, true); +} + +int prompt_transfer_amount(const string prompt, int max_amount) { + string input = ui_input_box("Inventory", prompt + " (max " + max_amount + ")", ""); + int amount = parse_int(input); + if (amount <= 0) return 0; + if (amount > max_amount) amount = max_amount; + return amount; +} + +void move_small_game_to_storage(int amount) { + for (int i = 0; i < amount; i++) { + string game_type = "small game"; + if (inv_small_game_types.length() > 0) { + game_type = inv_small_game_types[0]; + inv_small_game_types.remove_at(0); + } + storage_small_game_types.insert_last(game_type); + } +} + +void move_small_game_to_personal(int amount) { + for (int i = 0; i < amount; i++) { + string game_type = "small game"; + if (storage_small_game_types.length() > 0) { + game_type = storage_small_game_types[0]; + storage_small_game_types.remove_at(0); + } + inv_small_game_types.insert_last(game_type); + } +} + +void deposit_item(int item_type) { + int available = get_personal_count(item_type); + if (available <= 0) { + screen_reader_speak("Nothing to deposit.", true); + return; + } + int capacity = BASE_STORAGE_MAX - get_storage_count(item_type); + if (capacity <= 0) { + screen_reader_speak("Storage for that item is full.", true); + return; + } + int max_transfer = (available < capacity) ? available : capacity; + int amount = prompt_transfer_amount("Deposit how many?", max_transfer); + if (amount <= 0) return; + + if (item_type == ITEM_STICKS) { inv_sticks -= amount; storage_sticks += amount; } + else if (item_type == ITEM_VINES) { inv_vines -= amount; storage_vines += amount; } + else if (item_type == ITEM_REEDS) { inv_reeds -= amount; storage_reeds += amount; } + else if (item_type == ITEM_STONES) { inv_stones -= amount; storage_stones += amount; } + else if (item_type == ITEM_LOGS) { inv_logs -= amount; storage_logs += amount; } + else if (item_type == ITEM_CLAY) { inv_clay -= amount; storage_clay += amount; } + else if (item_type == ITEM_SMALL_GAME) { inv_small_game -= amount; storage_small_game += amount; move_small_game_to_storage(amount); } + else if (item_type == ITEM_MEAT) { inv_meat -= amount; storage_meat += amount; } + else if (item_type == ITEM_SKINS) { inv_skins -= amount; storage_skins += amount; } + else if (item_type == ITEM_SPEARS) { inv_spears -= amount; storage_spears += amount; } + else if (item_type == ITEM_SLINGS) { inv_slings -= amount; storage_slings += amount; } + else if (item_type == ITEM_AXES) { inv_axes -= amount; storage_axes += amount; } + else if (item_type == ITEM_SNARES) { inv_snares -= amount; storage_snares += amount; } + else if (item_type == ITEM_KNIVES) { inv_knives -= amount; storage_knives += amount; } + else if (item_type == ITEM_FISHING_POLES) { inv_fishing_poles -= amount; storage_fishing_poles += amount; } + else if (item_type == ITEM_ROPES) { inv_ropes -= amount; storage_ropes += amount; } + else if (item_type == ITEM_REED_BASKETS) { inv_reed_baskets -= amount; storage_reed_baskets += amount; } + else if (item_type == ITEM_CLAY_POTS) { inv_clay_pots -= amount; storage_clay_pots += amount; } + else if (item_type == ITEM_SKIN_HATS) { inv_skin_hats -= amount; storage_skin_hats += amount; } + else if (item_type == ITEM_SKIN_GLOVES) { inv_skin_gloves -= amount; storage_skin_gloves += amount; } + else if (item_type == ITEM_SKIN_PANTS) { inv_skin_pants -= amount; storage_skin_pants += amount; } + else if (item_type == ITEM_SKIN_TUNICS) { inv_skin_tunics -= amount; storage_skin_tunics += amount; } + else if (item_type == ITEM_MOCCASINS) { inv_moccasins -= amount; storage_moccasins += amount; } + else if (item_type == ITEM_SKIN_POUCHES) { inv_skin_pouches -= amount; storage_skin_pouches += amount; } + + cleanup_equipment_after_inventory_change(); + screen_reader_speak("Deposited " + amount + " " + get_item_label(item_type) + ".", true); +} + +void withdraw_item(int item_type) { + int available = get_storage_count(item_type); + if (available <= 0) { + screen_reader_speak("Nothing to withdraw.", true); + return; + } + int capacity = get_personal_stack_limit() - get_personal_count(item_type); + if (capacity <= 0) { + screen_reader_speak("You can't carry any more " + get_item_label(item_type) + ".", true); + return; + } + int max_transfer = (available < capacity) ? available : capacity; + int amount = prompt_transfer_amount("Withdraw how many?", max_transfer); + if (amount <= 0) return; + + if (item_type == ITEM_STICKS) { storage_sticks -= amount; inv_sticks += amount; } + else if (item_type == ITEM_VINES) { storage_vines -= amount; inv_vines += amount; } + else if (item_type == ITEM_REEDS) { storage_reeds -= amount; inv_reeds += amount; } + else if (item_type == ITEM_STONES) { storage_stones -= amount; inv_stones += amount; } + else if (item_type == ITEM_LOGS) { storage_logs -= amount; inv_logs += amount; } + else if (item_type == ITEM_CLAY) { storage_clay -= amount; inv_clay += amount; } + else if (item_type == ITEM_SMALL_GAME) { storage_small_game -= amount; inv_small_game += amount; move_small_game_to_personal(amount); } + else if (item_type == ITEM_MEAT) { storage_meat -= amount; inv_meat += amount; } + else if (item_type == ITEM_SKINS) { storage_skins -= amount; inv_skins += amount; } + else if (item_type == ITEM_SPEARS) { storage_spears -= amount; inv_spears += amount; } + else if (item_type == ITEM_SLINGS) { storage_slings -= amount; inv_slings += amount; } + else if (item_type == ITEM_AXES) { storage_axes -= amount; inv_axes += amount; } + else if (item_type == ITEM_SNARES) { storage_snares -= amount; inv_snares += amount; } + else if (item_type == ITEM_KNIVES) { storage_knives -= amount; inv_knives += amount; } + else if (item_type == ITEM_FISHING_POLES) { storage_fishing_poles -= amount; inv_fishing_poles += amount; } + else if (item_type == ITEM_ROPES) { storage_ropes -= amount; inv_ropes += amount; } + else if (item_type == ITEM_REED_BASKETS) { storage_reed_baskets -= amount; inv_reed_baskets += amount; } + else if (item_type == ITEM_CLAY_POTS) { storage_clay_pots -= amount; inv_clay_pots += amount; } + else if (item_type == ITEM_SKIN_HATS) { storage_skin_hats -= amount; inv_skin_hats += amount; } + else if (item_type == ITEM_SKIN_GLOVES) { storage_skin_gloves -= amount; inv_skin_gloves += amount; } + else if (item_type == ITEM_SKIN_PANTS) { storage_skin_pants -= amount; inv_skin_pants += amount; } + else if (item_type == ITEM_SKIN_TUNICS) { storage_skin_tunics -= amount; inv_skin_tunics += amount; } + else if (item_type == ITEM_MOCCASINS) { storage_moccasins -= amount; inv_moccasins += amount; } + else if (item_type == ITEM_SKIN_POUCHES) { storage_skin_pouches -= amount; inv_skin_pouches += amount; } + + screen_reader_speak("Withdrew " + amount + " " + get_item_label(item_type) + ".", true); +} + +void sacrifice_item(int item_type) { + int available = get_personal_count(item_type); + if (available <= 0) { + screen_reader_speak("Nothing to sacrifice.", true); + return; + } + + if (item_type == ITEM_STICKS) inv_sticks--; + else if (item_type == ITEM_VINES) inv_vines--; + else if (item_type == ITEM_REEDS) inv_reeds--; + else if (item_type == ITEM_STONES) inv_stones--; + else if (item_type == ITEM_LOGS) inv_logs--; + else if (item_type == ITEM_CLAY) inv_clay--; + else if (item_type == ITEM_SMALL_GAME) { + inv_small_game--; + if (inv_small_game_types.length() > 0) { + inv_small_game_types.remove_at(0); + } + } + else if (item_type == ITEM_MEAT) inv_meat--; + else if (item_type == ITEM_SKINS) inv_skins--; + else if (item_type == ITEM_SPEARS) inv_spears--; + else if (item_type == ITEM_SLINGS) inv_slings--; + else if (item_type == ITEM_AXES) inv_axes--; + else if (item_type == ITEM_SNARES) inv_snares--; + else if (item_type == ITEM_KNIVES) inv_knives--; + else if (item_type == ITEM_FISHING_POLES) inv_fishing_poles--; + else if (item_type == ITEM_ROPES) inv_ropes--; + else if (item_type == ITEM_REED_BASKETS) inv_reed_baskets--; + else if (item_type == ITEM_CLAY_POTS) inv_clay_pots--; + else if (item_type == ITEM_SKIN_HATS) inv_skin_hats--; + else if (item_type == ITEM_SKIN_GLOVES) inv_skin_gloves--; + else if (item_type == ITEM_SKIN_PANTS) inv_skin_pants--; + else if (item_type == ITEM_SKIN_TUNICS) inv_skin_tunics--; + else if (item_type == ITEM_MOCCASINS) inv_moccasins--; + else if (item_type == ITEM_SKIN_POUCHES) inv_skin_pouches--; + + cleanup_equipment_after_inventory_change(); + double favor_gain = get_item_favor_value(item_type); + favor += favor_gain; + screen_reader_speak("Sacrificed 1 " + get_item_label_singular(item_type) + ". Favor +" + format_favor(favor_gain) + ". Total " + format_favor(favor) + ".", true); +} + +void build_personal_inventory_options(string[]@ options, int[]@ item_types) { + options.resize(0); + item_types.resize(0); + options.insert_last("Sticks: " + inv_sticks); item_types.insert_last(ITEM_STICKS); + options.insert_last("Vines: " + inv_vines); item_types.insert_last(ITEM_VINES); + options.insert_last("Reeds: " + inv_reeds); item_types.insert_last(ITEM_REEDS); + options.insert_last("Stones: " + inv_stones); item_types.insert_last(ITEM_STONES); + options.insert_last("Logs: " + inv_logs); item_types.insert_last(ITEM_LOGS); + options.insert_last("Clay: " + inv_clay); item_types.insert_last(ITEM_CLAY); + options.insert_last("Small Game: " + inv_small_game); item_types.insert_last(ITEM_SMALL_GAME); + options.insert_last("Meat: " + inv_meat); item_types.insert_last(ITEM_MEAT); + options.insert_last("Skins: " + inv_skins); item_types.insert_last(ITEM_SKINS); + options.insert_last("Spears: " + inv_spears); item_types.insert_last(ITEM_SPEARS); + options.insert_last("Slings: " + inv_slings); item_types.insert_last(ITEM_SLINGS); + options.insert_last("Axes: " + inv_axes); item_types.insert_last(ITEM_AXES); + options.insert_last("Snares: " + inv_snares); item_types.insert_last(ITEM_SNARES); + options.insert_last("Knives: " + inv_knives); item_types.insert_last(ITEM_KNIVES); + options.insert_last("Fishing Poles: " + inv_fishing_poles); item_types.insert_last(ITEM_FISHING_POLES); + options.insert_last("Ropes: " + inv_ropes); item_types.insert_last(ITEM_ROPES); + options.insert_last("Reed Baskets: " + inv_reed_baskets); item_types.insert_last(ITEM_REED_BASKETS); + options.insert_last("Clay Pots: " + inv_clay_pots); item_types.insert_last(ITEM_CLAY_POTS); + options.insert_last("Skin Hats: " + inv_skin_hats); item_types.insert_last(ITEM_SKIN_HATS); + options.insert_last("Skin Gloves: " + inv_skin_gloves); item_types.insert_last(ITEM_SKIN_GLOVES); + options.insert_last("Skin Pants: " + inv_skin_pants); item_types.insert_last(ITEM_SKIN_PANTS); + options.insert_last("Skin Tunics: " + inv_skin_tunics); item_types.insert_last(ITEM_SKIN_TUNICS); + options.insert_last("Moccasins: " + inv_moccasins); item_types.insert_last(ITEM_MOCCASINS); + options.insert_last("Skin Pouches: " + inv_skin_pouches); item_types.insert_last(ITEM_SKIN_POUCHES); +} + +void build_storage_inventory_options(string[]@ options, int[]@ item_types) { + options.resize(0); + item_types.resize(0); + options.insert_last("Sticks: " + storage_sticks); item_types.insert_last(ITEM_STICKS); + options.insert_last("Vines: " + storage_vines); item_types.insert_last(ITEM_VINES); + options.insert_last("Reeds: " + storage_reeds); item_types.insert_last(ITEM_REEDS); + options.insert_last("Stones: " + storage_stones); item_types.insert_last(ITEM_STONES); + options.insert_last("Logs: " + storage_logs); item_types.insert_last(ITEM_LOGS); + options.insert_last("Clay: " + storage_clay); item_types.insert_last(ITEM_CLAY); + options.insert_last("Small Game: " + storage_small_game); item_types.insert_last(ITEM_SMALL_GAME); + options.insert_last("Meat: " + storage_meat); item_types.insert_last(ITEM_MEAT); + options.insert_last("Skins: " + storage_skins); item_types.insert_last(ITEM_SKINS); + options.insert_last("Spears: " + storage_spears); item_types.insert_last(ITEM_SPEARS); + options.insert_last("Slings: " + storage_slings); item_types.insert_last(ITEM_SLINGS); + options.insert_last("Axes: " + storage_axes); item_types.insert_last(ITEM_AXES); + options.insert_last("Snares: " + storage_snares); item_types.insert_last(ITEM_SNARES); + options.insert_last("Knives: " + storage_knives); item_types.insert_last(ITEM_KNIVES); + options.insert_last("Fishing Poles: " + storage_fishing_poles); item_types.insert_last(ITEM_FISHING_POLES); + options.insert_last("Ropes: " + storage_ropes); item_types.insert_last(ITEM_ROPES); + options.insert_last("Reed Baskets: " + storage_reed_baskets); item_types.insert_last(ITEM_REED_BASKETS); + options.insert_last("Clay Pots: " + storage_clay_pots); item_types.insert_last(ITEM_CLAY_POTS); + options.insert_last("Skin Hats: " + storage_skin_hats); item_types.insert_last(ITEM_SKIN_HATS); + options.insert_last("Skin Gloves: " + storage_skin_gloves); item_types.insert_last(ITEM_SKIN_GLOVES); + options.insert_last("Skin Pants: " + storage_skin_pants); item_types.insert_last(ITEM_SKIN_PANTS); + options.insert_last("Skin Tunics: " + storage_skin_tunics); item_types.insert_last(ITEM_SKIN_TUNICS); + options.insert_last("Moccasins: " + storage_moccasins); item_types.insert_last(ITEM_MOCCASINS); + options.insert_last("Skin Pouches: " + storage_skin_pouches); item_types.insert_last(ITEM_SKIN_POUCHES); +} + +void show_inventory() { + string info = "Inventory: "; + info += inv_sticks + " sticks, "; + info += inv_vines + " vines, "; + info += inv_reeds + " reeds, "; + info += inv_stones + " stones, "; + info += inv_logs + " logs, "; + info += inv_clay + " clay, "; + info += inv_small_game + " small game, "; + info += inv_meat + " meat, "; + info += inv_skins + " skins. "; + info += "Tools: " + inv_spears + " spears, " + inv_slings + " slings, " + inv_axes + " axes, " + inv_snares + " snares, " + inv_knives + " knives, " + inv_fishing_poles + " fishing poles, " + inv_ropes + " ropes, " + inv_reed_baskets + " reed baskets, " + inv_clay_pots + " clay pots. "; + info += "Clothing: " + inv_skin_hats + " hats, " + inv_skin_gloves + " gloves, " + inv_skin_pants + " pants, " + inv_skin_tunics + " tunics, " + inv_moccasins + " moccasins, " + inv_skin_pouches + " skin pouches."; + screen_reader_speak(info, true); +} + +void run_inventory_root_menu() { + screen_reader_speak("Inventory menu.", true); + + int selection = 0; + string[] options = {"Personal inventory", "Base storage"}; + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + if (selection == 0) run_inventory_menu(true); + else run_storage_menu(); + break; + } + } +} + +void run_inventory_menu(bool allow_deposit) { + screen_reader_speak("Inventory menu.", true); + + int selection = 0; + string[] options; + int[] item_types; + build_personal_inventory_options(options, item_types); + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], 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; + screen_reader_speak(options[selection], true); + } + } +} + +void run_storage_menu() { + if (world_storages.length() == 0) { + screen_reader_speak("No storage built.", true); + return; + } + + screen_reader_speak("Base storage.", true); + + int selection = 0; + string[] options; + int[] item_types; + build_storage_inventory_options(options, item_types); + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(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; + screen_reader_speak(options[selection], true); + } + } +} + +void run_altar_menu() { + if (world_altars.length() == 0) { + screen_reader_speak("No altar built.", true); + return; + } + + screen_reader_speak("Altar. Favor " + format_favor(favor) + ".", true); + + int selection = 0; + string[] options; + int[] item_types; + build_personal_inventory_options(options, item_types); + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(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; + screen_reader_speak(options[selection], true); + } + } +} + +void try_place_snare(int x) { + if (inv_snares > 0) { + // Prevent placing if one already exists here + if (get_snare_at(x) != null) { + screen_reader_speak("There is already a snare here.", true); + return; + } + + inv_snares--; + add_world_snare(x); + screen_reader_speak("Snare set.", true); + } else { + screen_reader_speak("No snares to place.", true); + } +} + +void try_feed_fire_stick(WorldFire@ fire) { + if (inv_sticks > 0 && fire != null) { + inv_sticks--; + fire.add_fuel(300000); // 5 minutes + screen_reader_speak("You dump an arm load of sticks into the fire.", true); + p.play_stationary("sounds/actions/feed_fire.ogg", false); + } +} + +void try_feed_fire_vine(WorldFire@ fire) { + if (inv_vines > 0 && fire != null) { + inv_vines--; + fire.add_fuel(60000); // 1 minute + screen_reader_speak("You toss a fiew vines and leaves into the fire.", true); + p.play_stationary("sounds/actions/feed_fire.ogg", false); + } +} + +void try_feed_fire_log(WorldFire@ fire) { + if (inv_logs > 0 && fire != null) { + inv_logs--; + fire.add_fuel(720000); // 12 minutes + screen_reader_speak("You heave a log into the fire.", true); + p.play_stationary("sounds/actions/feed_fire.ogg", 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) { + screen_reader_speak("Nothing to equip.", true); + } else { + run_equipment_menu(); + } + } +} + +void run_action_menu(int x) { + screen_reader_speak("Action menu.", true); + + int selection = 0; + string[] options; + int[] action_types; // Track what action each option corresponds to + + // Check if fire is nearby + WorldFire@ nearby_fire = get_fire_near(x); + bool can_feed_fire = nearby_fire != null; + + // Build menu options dynamically + options.insert_last("Place Snare"); + action_types.insert_last(0); + + if (can_feed_fire) { + if (inv_sticks > 0) { + options.insert_last("Feed fire with stick"); + action_types.insert_last(1); + } + if (inv_vines > 0) { + options.insert_last("Feed fire with vine"); + action_types.insert_last(2); + } + if (inv_logs > 0) { + options.insert_last("Feed fire with log"); + action_types.insert_last(3); + } + } + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + int action = action_types[selection]; + if (action == 0) { + try_place_snare(x); + } else if (action == 1) { + try_feed_fire_stick(nearby_fire); + } else if (action == 2) { + try_feed_fire_vine(nearby_fire); + } else if (action == 3) { + try_feed_fire_log(nearby_fire); + } + break; + } + } +} + +void run_equipment_menu() { + screen_reader_speak("Equipment menu.", true); + + int selection = 0; + string[] options; + int[] equipment_types; + + // Build menu dynamically based on what player has + if (inv_spears > 0) { + string status = equipment_is_equipped(EQUIP_SPEAR) ? " (equipped)" : ""; + options.insert_last("Spear" + status); + equipment_types.insert_last(EQUIP_SPEAR); + } + if (inv_slings > 0) { + string status = equipment_is_equipped(EQUIP_SLING) ? " (equipped)" : ""; + options.insert_last("Sling" + status); + equipment_types.insert_last(EQUIP_SLING); + } + if (inv_axes > 0) { + string status = equipment_is_equipped(EQUIP_AXE) ? " (equipped)" : ""; + options.insert_last("Stone Axe" + status); + equipment_types.insert_last(EQUIP_AXE); + } + if (inv_skin_hats > 0) { + string status = equipment_is_equipped(EQUIP_HAT) ? " (equipped)" : ""; + options.insert_last("Skin Hat" + status); + equipment_types.insert_last(EQUIP_HAT); + } + if (inv_skin_gloves > 0) { + string status = equipment_is_equipped(EQUIP_GLOVES) ? " (equipped)" : ""; + options.insert_last("Skin Gloves" + status); + equipment_types.insert_last(EQUIP_GLOVES); + } + if (inv_skin_pants > 0) { + string status = equipment_is_equipped(EQUIP_PANTS) ? " (equipped)" : ""; + options.insert_last("Skin Pants" + status); + equipment_types.insert_last(EQUIP_PANTS); + } + if (inv_skin_tunics > 0) { + string status = equipment_is_equipped(EQUIP_TUNIC) ? " (equipped)" : ""; + options.insert_last("Skin Tunic" + status); + equipment_types.insert_last(EQUIP_TUNIC); + } + if (inv_moccasins > 0) { + string status = equipment_is_equipped(EQUIP_MOCCASINS) ? " (equipped)" : ""; + options.insert_last("Moccasins" + status); + equipment_types.insert_last(EQUIP_MOCCASINS); + } + if (inv_skin_pouches > 0) { + string status = equipment_is_equipped(EQUIP_POUCH) ? " (equipped)" : ""; + options.insert_last("Skin Pouch" + status); + equipment_types.insert_last(EQUIP_POUCH); + } + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], true); + } + + int slot_index = get_quick_slot_key(); + if (slot_index != -1) { + int equip_type = equipment_types[selection]; + quick_slots[slot_index] = equip_type; + screen_reader_speak(get_equipment_name(equip_type) + " set to slot " + slot_index + ".", true); + } + + if (key_pressed(KEY_RETURN)) { + int equip_type = equipment_types[selection]; + if (equipment_is_equipped(equip_type)) { + unequip_equipment_type(equip_type); + screen_reader_speak(get_equipment_name(equip_type) + " unequipped.", true); + } else { + equip_equipment_type(equip_type); + screen_reader_speak(get_equipment_name(equip_type) + " equipped.", true); + } + update_max_health_from_equipment(); + break; + } + } +} diff --git a/src/notify.nvgt b/src/notify.nvgt index 986c388..327e8cf 100644 --- a/src/notify.nvgt +++ b/src/notify.nvgt @@ -2,13 +2,14 @@ string[] notification_history; const int MAX_NOTIFICATIONS = 10; int current_notification_index = -1; +string[] notification_queue; +int[] notification_sound_handles; void notify(string message) { // Play notification sound - p.play_stationary("sounds/notify.ogg", false); - - // Speak the message - screen_reader_speak(message, true); + int sound_handle = p.play_stationary("sounds/notify.ogg", false); + notification_queue.insert_last(message); + notification_sound_handles.insert_last(sound_handle); // Add to history notification_history.insert_last(message); @@ -22,6 +23,21 @@ void notify(string message) { current_notification_index = notification_history.length() - 1; } +void update_notifications() { + if (notification_queue.length() == 0) { + return; + } + + int sound_handle = notification_sound_handles[0]; + if (sound_handle != -1 && p.sound_is_playing(sound_handle)) { + return; + } + + screen_reader_speak(notification_queue[0], true); + notification_queue.remove_at(0); + notification_sound_handles.remove_at(0); +} + void check_notification_keys() { // [ for previous notification (older) with position if (key_pressed(KEY_LBRACKET)) { @@ -69,4 +85,3 @@ void check_notification_keys() { screen_reader_speak(notification_history[current_notification_index], true); } } - diff --git a/src/player.nvgt b/src/player.nvgt index 9899f00..3fe7007 100644 --- a/src/player.nvgt +++ b/src/player.nvgt @@ -13,6 +13,7 @@ timer climb_timer; // For climb speed // Health System int player_health = 10; +int base_max_health = 10; int max_health = 10; timer fire_damage_timer; timer healing_timer; @@ -23,6 +24,11 @@ timer sling_charge_timer; int sling_sound_handle = -1; int last_sling_stage = -1; // Track which stage we're in to avoid duplicate sounds +// Favor system +double favor = 0.0; +bool blessing_speed_active = false; +timer blessing_speed_timer; + // Timers timer walktimer; timer jumptimer; diff --git a/src/quest_system.nvgt b/src/quest_system.nvgt new file mode 100644 index 0000000..f935309 --- /dev/null +++ b/src/quest_system.nvgt @@ -0,0 +1,151 @@ +// Quest system +#include "src/quests/bat_invasion_game.nvgt" +#include "src/quests/enchanted_melody_game.nvgt" +#include "src/quests/escape_from_hel_game.nvgt" + +const int QUEST_BAT_INVASION = 0; +const int QUEST_ENCHANTED_MELODY = 1; +const int QUEST_ESCAPE_FROM_HEL = 2; +const int QUEST_TYPE_COUNT = 3; + +int[] quest_queue; +bool quest_roll_done_today = false; + +string get_quest_name(int quest_type) { + if (quest_type == QUEST_BAT_INVASION) return "Bat Invasion"; + if (quest_type == QUEST_ENCHANTED_MELODY) return "Enchanted Melody"; + if (quest_type == QUEST_ESCAPE_FROM_HEL) return "Escape from Hel"; + return "Unknown Quest"; +} + +string get_quest_description(int quest_type) { + if (quest_type == QUEST_BAT_INVASION) { + return "Bat Invasion. Giant killer bats are attacking. Press space to throw when the bat is centered."; + } + if (quest_type == QUEST_ENCHANTED_MELODY) { + return "Enchanted Melody. Repeat the pattern using E R D F or U I J K. Lowest to highest pitch."; + } + if (quest_type == QUEST_ESCAPE_FROM_HEL) { + return "Escape from Hel. Press space to jump over open graves. The pace quickens."; + } + return "Unknown quest."; +} + +int get_quest_chance_from_favor() { + int chance = int(favor * QUEST_CHANCE_PER_FAVOR); + if (chance < QUEST_MIN_CHANCE && favor > 1.0) chance = QUEST_MIN_CHANCE; + if (chance > 100) chance = 100; + return chance; +} + +void add_quest(int quest_type) { + if (quest_queue.length() >= QUEST_MAX_ACTIVE) return; + quest_queue.insert_last(quest_type); + notify("A new quest is available: " + get_quest_name(quest_type) + "."); +} + +void attempt_daily_quest() { + if (quest_roll_done_today) return; + if (favor <= 1.0) return; + if (world_altars.length() == 0) return; + if (quest_queue.length() >= QUEST_MAX_ACTIVE) return; + + quest_roll_done_today = true; + int chance = get_quest_chance_from_favor(); + int roll = random(1, 100); + if (roll > chance) return; + + int quest_type = random(0, QUEST_TYPE_COUNT - 1); + add_quest(quest_type); +} + +void check_quest_menu() { + if (key_pressed(KEY_Q)) { + if (x > BASE_END) { + screen_reader_speak("You are not in the base.", true); + return; + } + if (quest_queue.length() == 0) { + screen_reader_speak("No quests available.", true); + return; + } + run_quest_menu(); + } +} + +void apply_quest_reward(int score) { + if (score <= 0) { + screen_reader_speak("No reward earned.", true); + return; + } + + double favor_gain = score * QUEST_FAVOR_PER_POINT; + favor += favor_gain; + + int stones_gain = (score >= QUEST_STONE_SCORE) ? 1 : 0; + int logs_gain = (score >= QUEST_LOG_SCORE) ? 1 : 0; + int skins_gain = (score >= QUEST_SKIN_SCORE) ? 1 : 0; + + int stones_added = add_to_stack(inv_stones, stones_gain); + inv_stones += stones_added; + int logs_added = add_to_stack(inv_logs, logs_gain); + inv_logs += logs_added; + int skins_added = add_to_stack(inv_skins, skins_gain); + inv_skins += skins_added; + + string message = "Quest reward: favor +" + format_favor(favor_gain) + "."; + if (stones_gain > 0) message += " Stones +" + stones_added + "."; + if (logs_gain > 0) message += " Logs +" + logs_added + "."; + if (skins_gain > 0) message += " Skins +" + skins_added + "."; + screen_reader_speak(message, true); +} + +void run_quest(int quest_type) { + screen_reader_speak(get_quest_description(quest_type), true); + wait(800); + p.pause_all(); + int score = 0; + if (quest_type == QUEST_BAT_INVASION) score = run_bat_invasion(); + else if (quest_type == QUEST_ENCHANTED_MELODY) score = run_enchanted_melody(); + else if (quest_type == QUEST_ESCAPE_FROM_HEL) score = run_escape_from_hel(); + apply_quest_reward(score); + p.resume_all(); +} + +void run_quest_menu() { + screen_reader_speak("Quest menu.", true); + + int selection = 0; + string[] options; + for (uint i = 0; i < quest_queue.length(); i++) { + options.insert_last(get_quest_name(quest_queue[i])); + } + + while(true) { + wait(5); + menu_background_tick(); + if (key_pressed(KEY_ESCAPE)) { + screen_reader_speak("Closed.", true); + break; + } + + if (key_pressed(KEY_DOWN)) { + selection++; + if (selection >= options.length()) selection = 0; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_UP)) { + selection--; + if (selection < 0) selection = options.length() - 1; + screen_reader_speak(options[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + int quest_type = quest_queue[selection]; + quest_queue.remove_at(selection); + run_quest(quest_type); + break; + } + } +} diff --git a/src/quests/bat_invasion_game.nvgt b/src/quests/bat_invasion_game.nvgt new file mode 100644 index 0000000..6c0ec96 --- /dev/null +++ b/src/quests/bat_invasion_game.nvgt @@ -0,0 +1,99 @@ +// Bat Invasion quest game +string[] bat_sounds = {"sounds/quests/bat1.ogg", "sounds/quests/bat2.ogg"}; + +int run_bat_invasion() { + screen_reader_speak("Bat Invasion. Bats will fly past from left or right. Press space to throw your spear when the bat is centered. Press enter to continue.", true); + + // Wait for enter + while (!key_pressed(KEY_RETURN)) { + wait(5); + } + + screen_reader_speak("Starting.", true); + wait(500); + + int turns = 10; + int score = 0; + + // Position configuration + const int BAT_START_DISTANCE = 15; // How far left/right the bat starts + const int CENTER_TOLERANCE = 2; // How close to center counts as a hit + const int LISTENER_POS = 0; // Player is at center + + for (int i = 0; i < turns; i++) { + // Pick random bat sound + string bat_sound = bat_sounds[random(0, bat_sounds.length() - 1)]; + + // Randomly start from left or right + int direction = (random(0, 1) == 0) ? 1 : -1; // 1 = left to right, -1 = right to left + int start_pos = -BAT_START_DISTANCE * direction; + int end_pos = BAT_START_DISTANCE * direction; + + // Random flight speed (slower range with variation) + int flight_time = random(2200, 3400); + int update_interval = 30; // Update position every 30ms + + // Start bat sound at starting position + int bat_handle = p.play_1d(bat_sound, LISTENER_POS, start_pos, true); + + timer flight_timer; + flight_timer.restart(); + bool threw = false; + bool hit = false; + + while (flight_timer.elapsed < flight_time) { + wait(update_interval); + + // Calculate current position based on elapsed time + float progress = float(flight_timer.elapsed) / float(flight_time); + int current_pos = int(start_pos + (progress * (end_pos - start_pos))); + + // Update bat sound position + if (bat_handle != -1) { + p.update_sound_1d(bat_handle, current_pos); + } + + // Check for throw + if (!threw && key_pressed(KEY_SPACE)) { + threw = true; + p.play_stationary("sounds/quests/spear_throw.ogg", false); + + // Check if bat is centered (within tolerance) + int distance_from_center = current_pos; + if (distance_from_center < 0) distance_from_center = -distance_from_center; + + if (distance_from_center <= CENTER_TOLERANCE) { + hit = true; + // Stop bat sound and play hit + if (bat_handle != -1) { + p.destroy_sound(bat_handle); + bat_handle = -1; + } + wait(150); + p.play_stationary("sounds/quests/spear_hit.ogg", false); + score += 2; + break; + } else { + wait(150); + p.play_stationary("sounds/quests/spear_miss.ogg", false); + } + } + } + + // Clean up bat sound if still playing + if (bat_handle != -1) { + p.destroy_sound(bat_handle); + bat_handle = -1; + } + + // If didn't throw at all, count as miss + if (!threw) { + p.play_stationary("sounds/quests/spear_miss.ogg", false); + } + + wait(400); + } + + screen_reader_speak("Bat invasion complete. Score " + score + ".", true); + return score; +} diff --git a/src/quests/enchanted_melody_game.nvgt b/src/quests/enchanted_melody_game.nvgt new file mode 100644 index 0000000..ece2033 --- /dev/null +++ b/src/quests/enchanted_melody_game.nvgt @@ -0,0 +1,87 @@ +// Enchanted Melody quest game +string[] quest_notes = { + "sounds/quests/bone1.ogg", + "sounds/quests/bone2.ogg", + "sounds/quests/bone3.ogg", + "sounds/quests/bone4.ogg" +}; + +void play_note(int note_index) { + if (note_index < 0 || note_index >= int(quest_notes.length())) return; + p.play_stationary(quest_notes[note_index], false); +} + +int get_note_from_key() { + if (key_pressed(KEY_F) || key_pressed(KEY_K)) return 0; + if (key_pressed(KEY_D) || key_pressed(KEY_J)) return 1; + if (key_pressed(KEY_R) || key_pressed(KEY_I)) return 2; + if (key_pressed(KEY_E) || key_pressed(KEY_U)) return 3; + return -1; +} + +void run_practice_mode() { + screen_reader_speak("Enchanted Melody. Repeat the magical pattern using F D R E or K J I U, from lowest to highest pitch. Practice the notes now, then press enter to begin, or escape to cancel.", true); + + while (true) { + wait(5); + + // Check for practice note input + int note = get_note_from_key(); + if (note != -1) { + play_note(note); + } + + // Enter to start the game + if (key_pressed(KEY_RETURN)) { + return; + } + + // Escape to cancel + if (key_pressed(KEY_ESCAPE)) { + return; + } + } +} + +int run_enchanted_melody() { + // Practice mode first + run_practice_mode(); + + // Check if player pressed escape during practice + if (key_down(KEY_ESCAPE)) { + return 0; + } + + screen_reader_speak("Starting. Repeat the pattern.", true); + wait(500); + + int[] pattern; + pattern.insert_last(random(0, 3)); + int rounds = 0; + + while (true) { + for (uint i = 0; i < pattern.length(); i++) { + play_note(pattern[i]); + wait(600); + } + + uint index = 0; + while (index < pattern.length()) { + wait(5); + int note = get_note_from_key(); + if (note == -1) continue; + play_note(note); + if (note != pattern[index]) { + int score = rounds * 2; + screen_reader_speak("You matched " + rounds + " notes. Score " + score + ".", true); + return score; + } + index++; + } + + rounds++; + pattern.insert_last(random(0, 3)); + wait(500); + } + return 0; +} diff --git a/src/quests/escape_from_hel_game.nvgt b/src/quests/escape_from_hel_game.nvgt new file mode 100644 index 0000000..bbb7886 --- /dev/null +++ b/src/quests/escape_from_hel_game.nvgt @@ -0,0 +1,117 @@ +// Escape from Hel quest game +int run_escape_from_hel() { + screen_reader_speak("Escape from Hel. You are running from the Draugr, good luck. Press space to jump over open graves as you hear them approach. Press enter to continue.", true); + + // Wait for enter + while (!key_pressed(KEY_RETURN)) { + wait(5); + } + + screen_reader_speak("Starting.", true); + wait(500); + + int score = 0; + int steps_until_pit = 10; // First pit after exactly 10 steps + int steps_taken = 0; + int total_steps = 0; + int base_step_time = 900; + string step_sound = "sounds/quests/footstep.ogg"; + + // Pit audio configuration + int pit_fade_start = 6; // Start pit sound 6 steps before the pit + float start_volume = -35.0; // Start very quiet + float max_volume = 0.0; // Full volume at pit + int pit_handle = -1; + + // Jump state + bool jumping = false; + timer jump_timer; + const int JUMP_DURATION = 400; // How long a jump lasts + + while (true) { + // Speed increases indefinitely until player can't keep up + int step_time = base_step_time - (total_steps * 8); + if (step_time < 50) step_time = 50; // Minimum to prevent audio/timing issues + + // Check if jump finished + if (jumping && jump_timer.elapsed >= JUMP_DURATION) { + jumping = false; + } + + // Process step with input checking + timer step_timer; + step_timer.restart(); + + // Play footstep at start of step + if (!jumping) { + p.play_stationary(step_sound, false); + } + + // Wait for step duration, checking for jump input + while (step_timer.elapsed < step_time) { + wait(5); + + // Allow jump anytime when not already jumping + if (!jumping && key_pressed(KEY_SPACE)) { + p.play_stationary("sounds/quests/jump.ogg", false); + jumping = true; + jump_timer.restart(); + } + + // Update jump state during wait + if (jumping && jump_timer.elapsed >= JUMP_DURATION) { + jumping = false; + } + } + + steps_taken++; + total_steps++; + + // Calculate steps remaining until pit + int steps_remaining = steps_until_pit - steps_taken; + + // Start pit sound when approaching + if (steps_remaining <= pit_fade_start && steps_remaining >= 0 && pit_handle == -1) { + pit_handle = p.play_stationary("sounds/quests/pit.ogg", true); + if (pit_handle != -1) { + p.update_sound_start_values(pit_handle, 0.0, start_volume, 1.0); + } + } + + // Update pit volume as we get closer + if (pit_handle != -1 && steps_remaining >= 0) { + float progress = float(pit_fade_start - steps_remaining) / float(pit_fade_start); + if (progress > 1.0) progress = 1.0; + float current_volume = start_volume + (progress * (max_volume - start_volume)); + p.update_sound_start_values(pit_handle, 0.0, current_volume, 1.0); + } + + // Reached the pit + if (steps_taken >= steps_until_pit) { + // Must be jumping to clear the pit + if (!jumping) { + // Fell in + if (pit_handle != -1) { + p.destroy_sound(pit_handle); + pit_handle = -1; + } + p.play_stationary("sounds/quests/fall.ogg", false); + screen_reader_speak("You fell in. Score " + score + ".", true); + return score; + } + + // Successfully jumped over + if (pit_handle != -1) { + p.destroy_sound(pit_handle); + pit_handle = -1; + } + + score += 2; + + // Reset for next pit - random spacing between 5-15 steps + steps_taken = 0; + steps_until_pit = random(5, 15); + } + } + return score; +} diff --git a/src/save_system.nvgt b/src/save_system.nvgt index c3269e4..597b2c7 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -3,6 +3,7 @@ const string SAVE_FILE_PATH = "save.dat"; const string SAVE_ENCRYPTION_KEY = "draugnorak_save_v1"; const int SAVE_VERSION = 1; +string last_save_error = ""; bool has_save_game() { return file_exists(SAVE_FILE_PATH); @@ -47,15 +48,42 @@ bool read_file_string(const string&in filename, string&out data) { double get_number(dictionary@ data, const string&in key, double defaultValue) { double value; if (@data == null) return defaultValue; - if (!data.get(key, value)) return defaultValue; - return value; + if (data.get(key, value)) return value; + int value_int; + if (data.get(key, value_int)) return value_int; + string value_str; + if (data.get(key, value_str)) { + return parse_int(value_str); + } + return defaultValue; } bool get_bool(dictionary@ data, const string&in key, bool defaultValue) { bool value; if (@data == null) return defaultValue; - if (!data.get(key, value)) return defaultValue; - return value; + if (data.get(key, value)) return value; + int value_int; + if (data.get(key, value_int)) return value_int != 0; + string value_str; + if (data.get(key, value_str)) return value_str == "1" || value_str == "true"; + return defaultValue; +} + +bool dictionary_has_keys(dictionary@ data) { + if (@data == null) return false; + string[]@ keys = data.get_keys(); + return keys.length() > 0; +} + +bool has_number_key(dictionary@ data, const string&in key) { + double value; + if (@data == null) return false; + if (data.get(key, value)) return true; + int value_int; + if (data.get(key, value_int)) return true; + string value_str; + if (data.get(key, value_str)) return value_str.length() > 0; + return false; } string[] get_string_list(dictionary@ data, const string&in key) { @@ -102,6 +130,10 @@ void clear_world_objects() { world_firepits.resize(0); world_herb_gardens.resize(0); + world_storages.resize(0); + world_pastures.resize(0); + world_stables.resize(0); + world_altars.resize(0); for (uint i = 0; i < trees.length(); i++) { if (trees[i].sound_handle != -1) { @@ -131,11 +163,15 @@ void reset_game_state() { searching = false; player_health = 10; + base_max_health = 10; max_health = 10; + favor = 0.0; + blessing_speed_active = false; inv_stones = 0; inv_sticks = 0; inv_vines = 0; + inv_reeds = 0; inv_logs = 0; inv_clay = 0; inv_small_game = 0; @@ -148,10 +184,52 @@ void reset_game_state() { inv_knives = 0; inv_fishing_poles = 0; inv_slings = 0; + inv_ropes = 0; + inv_reed_baskets = 0; + inv_clay_pots = 0; + inv_skin_hats = 0; + inv_skin_gloves = 0; + inv_skin_pants = 0; + inv_skin_tunics = 0; + inv_moccasins = 0; + inv_skin_pouches = 0; + storage_stones = 0; + storage_sticks = 0; + storage_vines = 0; + storage_reeds = 0; + storage_logs = 0; + storage_clay = 0; + storage_small_game = 0; + storage_small_game_types.resize(0); + storage_meat = 0; + storage_skins = 0; + storage_spears = 0; + storage_snares = 0; + storage_axes = 0; + storage_knives = 0; + storage_fishing_poles = 0; + storage_slings = 0; + storage_ropes = 0; + storage_reed_baskets = 0; + storage_clay_pots = 0; + storage_skin_hats = 0; + storage_skin_gloves = 0; + storage_skin_pants = 0; + storage_skin_tunics = 0; + storage_moccasins = 0; + storage_skin_pouches = 0; spear_equipped = false; axe_equipped = false; sling_equipped = false; + equipped_head = EQUIP_NONE; + equipped_torso = EQUIP_NONE; + equipped_arms = EQUIP_NONE; + equipped_hands = EQUIP_NONE; + equipped_legs = EQUIP_NONE; + equipped_feet = EQUIP_NONE; + reset_quick_slots(); + update_max_health_from_equipment(); MAP_SIZE = 35; expanded_area_start = -1; @@ -160,15 +238,24 @@ void reset_game_state() { barricade_health = 0; barricade_initialized = false; + residents_count = 0; current_hour = 8; current_day = 1; is_daytime = true; sun_setting_warned = false; sunrise_warned = false; + crossfade_active = false; + crossfade_to_night = false; area_expanded_today = false; invasion_active = false; invasion_start_hour = -1; + invasion_chance = 25; + invasion_triggered_today = false; + invasion_roll_done_today = false; + invasion_scheduled_hour = -1; + quest_roll_done_today = false; + quest_queue.resize(0); walktimer.restart(); jumptimer.restart(); @@ -188,6 +275,7 @@ void start_new_game() { spawn_trees(5, 19); init_barricade(); init_time(); + save_game_state(); } string serialize_bool(bool value) { @@ -210,6 +298,181 @@ string serialize_stream(WorldStream@ stream) { return stream.start_position + "|" + stream.get_width(); } +string serialize_bandit(Bandit@ bandit) { + return bandit.position + "|" + bandit.health + "|" + bandit.weapon_type + "|" + bandit.behavior_state + "|" + bandit.wander_direction + "|" + bandit.move_interval; +} + +string join_string_array(const string[]@ arr) { + if (@arr == null || arr.length() == 0) return ""; + string result = arr[0]; + for (uint i = 1; i < arr.length(); i++) { + result += "\n" + arr[i]; + } + return result; +} + +string[] split_string_array(const string&in data) { + string[] result; + if (data.length() == 0) return result; + result = data.split("\n"); + return result; +} + +string[] get_string_list_or_split(dictionary@ data, const string&in key) { + string[] result = get_string_list(data, key); + if (result.length() > 0) return result; + string value; + if (@data != null && data.get(key, value)) { + return split_string_array(value); + } + return result; +} + +int get_byte_at(const string&in data, int index) { + string single = data.substr(index, 1); + return character_to_ascii(single); +} + +bool find_raw_key(const string&in rawData, const string&in key, int &out pos_after_key) { + int key_len = key.length(); + if (key_len <= 0 || key_len > 65535) return false; + int low = key_len & 0xFF; + int high = (key_len >> 8) & 0xFF; + int limit = rawData.length() - key_len - 2; + for (int i = 0; i <= limit; i++) { + if (get_byte_at(rawData, i) != low) continue; + if (get_byte_at(rawData, i + 1) != high) continue; + if (rawData.substr(i + 2, key_len) == key) { + pos_after_key = i + 2 + key_len; + return true; + } + } + return false; +} + +bool get_raw_number(const string&in rawData, const string&in key, int &out value) { + int pos; + if (!find_raw_key(rawData, key, pos)) return false; + if (pos >= rawData.length()) return false; + int type = get_byte_at(rawData, pos); + if (type != 2) return false; + if (pos + 1 + 8 > rawData.length()) return false; + double result = 0; + double multiplier = 1; + for (int i = 0; i < 8; i++) { + result += get_byte_at(rawData, pos + 1 + i) * multiplier; + multiplier *= 256; + } + value = int(result); + return true; +} + +bool get_raw_bool(const string&in rawData, const string&in key, bool &out value) { + int pos; + if (!find_raw_key(rawData, key, pos)) return false; + if (pos >= rawData.length()) return false; + int type = get_byte_at(rawData, pos); + if (type != 1) return false; + if (pos + 1 >= rawData.length()) return false; + value = (get_byte_at(rawData, pos + 1) != 0); + return true; +} + +bool load_game_state_from_raw(const string&in rawData) { + reset_game_state(); + + int value; + bool bool_value; + + if (!get_raw_number(rawData, "player_x", value)) return false; + x = value; + if (!get_raw_number(rawData, "player_health", value)) return false; + player_health = value; + if (!get_raw_number(rawData, "time_current_day", value)) return false; + current_day = value; + + if (get_raw_number(rawData, "player_y", value)) y = value; + if (get_raw_number(rawData, "player_facing", value)) facing = value; + 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, "time_current_hour", value)) current_hour = value; + if (get_raw_number(rawData, "world_map_size", value)) MAP_SIZE = value; + if (get_raw_number(rawData, "world_expanded_area_start", value)) expanded_area_start = value; + if (get_raw_number(rawData, "world_expanded_area_end", value)) expanded_area_end = value; + if (get_raw_number(rawData, "world_barricade_health", value)) barricade_health = value; + if (get_raw_number(rawData, "world_residents_count", value)) residents_count = value; + + if (get_raw_bool(rawData, "world_barricade_initialized", bool_value)) barricade_initialized = bool_value; + if (get_raw_bool(rawData, "time_sun_setting_warned", bool_value)) sun_setting_warned = bool_value; + if (get_raw_bool(rawData, "time_sunrise_warned", bool_value)) sunrise_warned = bool_value; + if (get_raw_bool(rawData, "time_area_expanded_today", bool_value)) area_expanded_today = bool_value; + if (get_raw_bool(rawData, "time_invasion_active", bool_value)) invasion_active = bool_value; + if (get_raw_bool(rawData, "time_invasion_triggered_today", bool_value)) invasion_triggered_today = bool_value; + if (get_raw_bool(rawData, "time_invasion_roll_done_today", bool_value)) invasion_roll_done_today = bool_value; + if (get_raw_bool(rawData, "quest_roll_done_today", bool_value)) quest_roll_done_today = bool_value; + + if (get_raw_number(rawData, "time_invasion_start_hour", value)) invasion_start_hour = value; + if (get_raw_number(rawData, "time_invasion_chance", value)) invasion_chance = value; + if (get_raw_number(rawData, "time_invasion_scheduled_hour", value)) invasion_scheduled_hour = value; + + if (get_raw_number(rawData, "inventory_stones", value)) inv_stones = value; + if (get_raw_number(rawData, "inventory_sticks", value)) inv_sticks = value; + if (get_raw_number(rawData, "inventory_vines", value)) inv_vines = value; + if (get_raw_number(rawData, "inventory_reeds", value)) inv_reeds = value; + if (get_raw_number(rawData, "inventory_logs", value)) inv_logs = value; + if (get_raw_number(rawData, "inventory_clay", value)) inv_clay = value; + if (get_raw_number(rawData, "inventory_small_game", value)) inv_small_game = value; + if (get_raw_number(rawData, "inventory_meat", value)) inv_meat = value; + if (get_raw_number(rawData, "inventory_skins", value)) inv_skins = value; + if (get_raw_number(rawData, "inventory_spears", value)) inv_spears = value; + if (get_raw_number(rawData, "inventory_snares", value)) inv_snares = value; + if (get_raw_number(rawData, "inventory_axes", value)) inv_axes = value; + if (get_raw_number(rawData, "inventory_knives", value)) inv_knives = value; + if (get_raw_number(rawData, "inventory_fishing_poles", value)) inv_fishing_poles = value; + if (get_raw_number(rawData, "inventory_slings", value)) inv_slings = value; + if (get_raw_number(rawData, "inventory_ropes", value)) inv_ropes = value; + if (get_raw_number(rawData, "inventory_reed_baskets", value)) inv_reed_baskets = value; + if (get_raw_number(rawData, "inventory_clay_pots", value)) inv_clay_pots = value; + if (get_raw_number(rawData, "inventory_skin_hats", value)) inv_skin_hats = value; + if (get_raw_number(rawData, "inventory_skin_gloves", value)) inv_skin_gloves = value; + if (get_raw_number(rawData, "inventory_skin_pants", value)) inv_skin_pants = value; + if (get_raw_number(rawData, "inventory_skin_tunics", value)) inv_skin_tunics = value; + if (get_raw_number(rawData, "inventory_moccasins", value)) inv_moccasins = value; + if (get_raw_number(rawData, "inventory_skin_pouches", value)) inv_skin_pouches = value; + + if (get_raw_bool(rawData, "equipment_spear_equipped", bool_value)) spear_equipped = bool_value; + if (get_raw_bool(rawData, "equipment_axe_equipped", bool_value)) axe_equipped = bool_value; + if (get_raw_bool(rawData, "equipment_sling_equipped", bool_value)) sling_equipped = bool_value; + if (get_raw_number(rawData, "equipment_head", value)) equipped_head = value; + if (get_raw_number(rawData, "equipment_torso", value)) equipped_torso = value; + if (get_raw_number(rawData, "equipment_hands", value)) equipped_hands = value; + if (get_raw_number(rawData, "equipment_legs", value)) equipped_legs = value; + if (get_raw_number(rawData, "equipment_feet", value)) equipped_feet = value; + if (get_raw_number(rawData, "equipment_arms", value)) equipped_arms = value; + + if (equipped_arms != EQUIP_POUCH) equipped_arms = EQUIP_NONE; + if (equipped_arms == EQUIP_POUCH && inv_skin_pouches <= 0) equipped_arms = EQUIP_NONE; + + if (inv_small_game_types.length() == 0 && inv_small_game > 0) { + for (int i = 0; i < inv_small_game; i++) { + inv_small_game_types.insert_last("small game"); + } + } + + if (!barricade_initialized) { + init_barricade(); + } else { + if (barricade_health < 0) barricade_health = 0; + if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; + } + spawn_trees(5, 19); + + is_daytime = (current_hour >= 6 && current_hour < 19); + hour_timer.restart(); + update_max_health_from_equipment(); + return true; +} bool save_game_state() { dictionary saveData; @@ -218,11 +481,14 @@ bool save_game_state() { saveData.set("player_y", y); saveData.set("player_facing", facing); saveData.set("player_health", player_health); + saveData.set("player_base_health", base_max_health); saveData.set("player_max_health", max_health); + saveData.set("player_favor", favor); saveData.set("inventory_stones", inv_stones); saveData.set("inventory_sticks", inv_sticks); saveData.set("inventory_vines", inv_vines); + saveData.set("inventory_reeds", inv_reeds); saveData.set("inventory_logs", inv_logs); saveData.set("inventory_clay", inv_clay); saveData.set("inventory_small_game", inv_small_game); @@ -234,11 +500,57 @@ bool save_game_state() { saveData.set("inventory_knives", inv_knives); saveData.set("inventory_fishing_poles", inv_fishing_poles); saveData.set("inventory_slings", inv_slings); - saveData.set("inventory_small_game_types", inv_small_game_types); + saveData.set("inventory_ropes", inv_ropes); + saveData.set("inventory_reed_baskets", inv_reed_baskets); + saveData.set("inventory_clay_pots", inv_clay_pots); + saveData.set("inventory_skin_hats", inv_skin_hats); + saveData.set("inventory_skin_gloves", inv_skin_gloves); + saveData.set("inventory_skin_pants", inv_skin_pants); + 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_small_game_types", join_string_array(inv_small_game_types)); + + saveData.set("storage_stones", storage_stones); + saveData.set("storage_sticks", storage_sticks); + saveData.set("storage_vines", storage_vines); + saveData.set("storage_reeds", storage_reeds); + saveData.set("storage_logs", storage_logs); + saveData.set("storage_clay", storage_clay); + saveData.set("storage_small_game", storage_small_game); + saveData.set("storage_meat", storage_meat); + saveData.set("storage_skins", storage_skins); + saveData.set("storage_spears", storage_spears); + saveData.set("storage_snares", storage_snares); + saveData.set("storage_axes", storage_axes); + saveData.set("storage_knives", storage_knives); + saveData.set("storage_fishing_poles", storage_fishing_poles); + saveData.set("storage_slings", storage_slings); + saveData.set("storage_ropes", storage_ropes); + saveData.set("storage_reed_baskets", storage_reed_baskets); + saveData.set("storage_clay_pots", storage_clay_pots); + saveData.set("storage_skin_hats", storage_skin_hats); + saveData.set("storage_skin_gloves", storage_skin_gloves); + saveData.set("storage_skin_pants", storage_skin_pants); + 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_small_game_types", join_string_array(storage_small_game_types)); saveData.set("equipment_spear_equipped", spear_equipped); saveData.set("equipment_axe_equipped", axe_equipped); saveData.set("equipment_sling_equipped", sling_equipped); + saveData.set("equipment_head", equipped_head); + saveData.set("equipment_torso", equipped_torso); + saveData.set("equipment_arms", equipped_arms); + saveData.set("equipment_hands", equipped_hands); + saveData.set("equipment_legs", equipped_legs); + saveData.set("equipment_feet", equipped_feet); + string[] quickSlotData; + for (uint i = 0; i < quick_slots.length(); i++) { + quickSlotData.insert_last("" + quick_slots[i]); + } + saveData.set("equipment_quick_slots", join_string_array(quickSlotData)); saveData.set("time_current_hour", current_hour); saveData.set("time_current_day", current_day); @@ -248,49 +560,90 @@ bool save_game_state() { saveData.set("time_area_expanded_today", area_expanded_today); saveData.set("time_invasion_active", invasion_active); saveData.set("time_invasion_start_hour", invasion_start_hour); + saveData.set("time_invasion_chance", invasion_chance); + 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("quest_roll_done_today", quest_roll_done_today); + string[] questData; + for (uint i = 0; i < quest_queue.length(); i++) { + questData.insert_last("" + quest_queue[i]); + } + saveData.set("quest_queue", join_string_array(questData)); saveData.set("world_map_size", MAP_SIZE); saveData.set("world_expanded_area_start", expanded_area_start); saveData.set("world_expanded_area_end", expanded_area_end); saveData.set("world_barricade_initialized", barricade_initialized); saveData.set("world_barricade_health", barricade_health); - saveData.set("world_expanded_terrain_types", expanded_terrain_types); + saveData.set("world_residents_count", residents_count); + saveData.set("world_expanded_terrain_types", join_string_array(expanded_terrain_types)); string[] treeData; for (uint i = 0; i < trees.length(); i++) { treeData.insert_last(serialize_tree(trees[i])); } - saveData.set("trees_data", treeData); + saveData.set("trees_data", join_string_array(treeData)); string[] snareData; for (uint i = 0; i < world_snares.length(); i++) { snareData.insert_last(serialize_snare(world_snares[i])); } - saveData.set("snares_data", snareData); + saveData.set("snares_data", join_string_array(snareData)); string[] fireData; for (uint i = 0; i < world_fires.length(); i++) { fireData.insert_last(serialize_fire(world_fires[i])); } - saveData.set("fires_data", fireData); + saveData.set("fires_data", join_string_array(fireData)); string[] firepitPositions; for (uint i = 0; i < world_firepits.length(); i++) { firepitPositions.insert_last("" + world_firepits[i].position); } - saveData.set("firepits_positions", firepitPositions); + saveData.set("firepits_positions", join_string_array(firepitPositions)); string[] herbPositions; for (uint i = 0; i < world_herb_gardens.length(); i++) { herbPositions.insert_last("" + world_herb_gardens[i].position); } - saveData.set("herb_gardens_positions", herbPositions); + saveData.set("herb_gardens_positions", join_string_array(herbPositions)); + + string[] storagePositions; + for (uint i = 0; i < world_storages.length(); i++) { + storagePositions.insert_last("" + world_storages[i].position); + } + saveData.set("storages_positions", join_string_array(storagePositions)); + + string[] pasturePositions; + for (uint i = 0; i < world_pastures.length(); i++) { + pasturePositions.insert_last("" + world_pastures[i].position); + } + saveData.set("pastures_positions", join_string_array(pasturePositions)); + + string[] stablePositions; + for (uint i = 0; i < world_stables.length(); i++) { + stablePositions.insert_last("" + world_stables[i].position); + } + saveData.set("stables_positions", join_string_array(stablePositions)); + + string[] altarPositions; + for (uint i = 0; i < world_altars.length(); i++) { + altarPositions.insert_last("" + world_altars[i].position); + } + saveData.set("altars_positions", join_string_array(altarPositions)); string[] streamData; for (uint i = 0; i < world_streams.length(); i++) { streamData.insert_last(serialize_stream(world_streams[i])); } - saveData.set("streams_data", streamData); + saveData.set("streams_data", join_string_array(streamData)); + + string[] banditData; + for (uint i = 0; i < bandits.length(); i++) { + banditData.insert_last(serialize_bandit(bandits[i])); + } + saveData.set("bandits_data", join_string_array(banditData)); string rawData = saveData.serialize(); string encryptedData = encrypt_save_data(rawData); @@ -298,23 +651,38 @@ bool save_game_state() { } bool load_game_state() { + last_save_error = ""; if (!file_exists(SAVE_FILE_PATH)) { + last_save_error = "No save file found."; return false; } string encryptedData; if (!read_file_string(SAVE_FILE_PATH, encryptedData)) { + last_save_error = "Unable to read save file."; return false; } string rawData = decrypt_save_data(encryptedData); - if (rawData.length() == 0) { + dictionary@ saveData = deserialize(rawData); + if (@saveData == null || !dictionary_has_keys(saveData)) { + saveData = deserialize(encryptedData); + } + if (@saveData == null || !dictionary_has_keys(saveData)) { + if (load_game_state_from_raw(rawData)) { + return true; + } + last_save_error = "Save file is corrupted or unreadable."; return false; } - - dictionary@ saveData = deserialize(rawData); - if (@saveData == null) { - return false; + double version; + bool has_version = saveData.get("version", version); + if (!has_version) { + if (!has_number_key(saveData, "player_x") || !has_number_key(saveData, "player_health") || !has_number_key(saveData, "time_current_day")) { + last_save_error = "Save file is missing required data."; + return false; + } + version = 0; } reset_game_state(); @@ -323,7 +691,7 @@ bool load_game_state() { expanded_area_start = int(get_number(saveData, "world_expanded_area_start", -1)); expanded_area_end = int(get_number(saveData, "world_expanded_area_end", -1)); - string[] loadedTerrain = get_string_list(saveData, "world_expanded_terrain_types"); + string[] loadedTerrain = get_string_list_or_split(saveData, "world_expanded_terrain_types"); expanded_terrain_types.resize(0); for (uint i = 0; i < loadedTerrain.length(); i++) { expanded_terrain_types.insert_last(loadedTerrain[i]); @@ -331,6 +699,8 @@ bool load_game_state() { barricade_initialized = get_bool(saveData, "world_barricade_initialized", true); barricade_health = int(get_number(saveData, "world_barricade_health", BARRICADE_BASE_HEALTH)); + residents_count = int(get_number(saveData, "world_residents_count", 0)); + if (residents_count < 0) residents_count = 0; if (!barricade_initialized) { init_barricade(); } else { @@ -343,6 +713,8 @@ bool load_game_state() { facing = int(get_number(saveData, "player_facing", 1)); player_health = int(get_number(saveData, "player_health", 10)); 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); if (x < 0) x = 0; if (x >= MAP_SIZE) x = MAP_SIZE - 1; @@ -352,6 +724,7 @@ bool load_game_state() { inv_stones = int(get_number(saveData, "inventory_stones", 0)); inv_sticks = int(get_number(saveData, "inventory_sticks", 0)); inv_vines = int(get_number(saveData, "inventory_vines", 0)); + inv_reeds = int(get_number(saveData, "inventory_reeds", 0)); inv_logs = int(get_number(saveData, "inventory_logs", 0)); inv_clay = int(get_number(saveData, "inventory_clay", 0)); inv_small_game = int(get_number(saveData, "inventory_small_game", 0)); @@ -363,8 +736,17 @@ bool load_game_state() { inv_knives = int(get_number(saveData, "inventory_knives", 0)); inv_fishing_poles = int(get_number(saveData, "inventory_fishing_poles", 0)); inv_slings = int(get_number(saveData, "inventory_slings", 0)); + inv_ropes = int(get_number(saveData, "inventory_ropes", 0)); + inv_reed_baskets = int(get_number(saveData, "inventory_reed_baskets", 0)); + inv_clay_pots = int(get_number(saveData, "inventory_clay_pots", 0)); + inv_skin_hats = int(get_number(saveData, "inventory_skin_hats", 0)); + inv_skin_gloves = int(get_number(saveData, "inventory_skin_gloves", 0)); + inv_skin_pants = int(get_number(saveData, "inventory_skin_pants", 0)); + 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)); - string[] loadedSmallGameTypes = get_string_list(saveData, "inventory_small_game_types"); + string[] loadedSmallGameTypes = get_string_list_or_split(saveData, "inventory_small_game_types"); inv_small_game_types.resize(0); for (uint i = 0; i < loadedSmallGameTypes.length(); i++) { inv_small_game_types.insert_last(loadedSmallGameTypes[i]); @@ -378,9 +760,71 @@ bool load_game_state() { inv_small_game = inv_small_game_types.length(); } + storage_stones = int(get_number(saveData, "storage_stones", 0)); + storage_sticks = int(get_number(saveData, "storage_sticks", 0)); + storage_vines = int(get_number(saveData, "storage_vines", 0)); + storage_reeds = int(get_number(saveData, "storage_reeds", 0)); + storage_logs = int(get_number(saveData, "storage_logs", 0)); + storage_clay = int(get_number(saveData, "storage_clay", 0)); + storage_small_game = int(get_number(saveData, "storage_small_game", 0)); + storage_meat = int(get_number(saveData, "storage_meat", 0)); + storage_skins = int(get_number(saveData, "storage_skins", 0)); + storage_spears = int(get_number(saveData, "storage_spears", 0)); + storage_snares = int(get_number(saveData, "storage_snares", 0)); + storage_axes = int(get_number(saveData, "storage_axes", 0)); + storage_knives = int(get_number(saveData, "storage_knives", 0)); + storage_fishing_poles = int(get_number(saveData, "storage_fishing_poles", 0)); + storage_slings = int(get_number(saveData, "storage_slings", 0)); + storage_ropes = int(get_number(saveData, "storage_ropes", 0)); + storage_reed_baskets = int(get_number(saveData, "storage_reed_baskets", 0)); + storage_clay_pots = int(get_number(saveData, "storage_clay_pots", 0)); + storage_skin_hats = int(get_number(saveData, "storage_skin_hats", 0)); + storage_skin_gloves = int(get_number(saveData, "storage_skin_gloves", 0)); + storage_skin_pants = int(get_number(saveData, "storage_skin_pants", 0)); + 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)); + + string[] loadedStorageSmallGameTypes = get_string_list_or_split(saveData, "storage_small_game_types"); + storage_small_game_types.resize(0); + for (uint i = 0; i < loadedStorageSmallGameTypes.length(); i++) { + storage_small_game_types.insert_last(loadedStorageSmallGameTypes[i]); + } + if (storage_small_game_types.length() == 0 && storage_small_game > 0) { + for (int i = 0; i < storage_small_game; i++) { + storage_small_game_types.insert_last("small game"); + } + } else { + storage_small_game = storage_small_game_types.length(); + } + spear_equipped = get_bool(saveData, "equipment_spear_equipped", false); axe_equipped = get_bool(saveData, "equipment_axe_equipped", false); sling_equipped = get_bool(saveData, "equipment_sling_equipped", false); + equipped_head = int(get_number(saveData, "equipment_head", EQUIP_NONE)); + equipped_torso = int(get_number(saveData, "equipment_torso", EQUIP_NONE)); + equipped_arms = int(get_number(saveData, "equipment_arms", EQUIP_NONE)); + equipped_hands = int(get_number(saveData, "equipment_hands", EQUIP_NONE)); + equipped_legs = int(get_number(saveData, "equipment_legs", EQUIP_NONE)); + equipped_feet = int(get_number(saveData, "equipment_feet", EQUIP_NONE)); + if (equipped_head != EQUIP_HAT) equipped_head = EQUIP_NONE; + if (equipped_torso != EQUIP_TUNIC) equipped_torso = EQUIP_NONE; + if (equipped_hands != EQUIP_GLOVES) equipped_hands = EQUIP_NONE; + if (equipped_legs != EQUIP_PANTS) equipped_legs = EQUIP_NONE; + if (equipped_feet != EQUIP_MOCCASINS) equipped_feet = EQUIP_NONE; + if (equipped_arms != EQUIP_POUCH) equipped_arms = EQUIP_NONE; + if (equipped_arms == EQUIP_POUCH && inv_skin_pouches <= 0) equipped_arms = EQUIP_NONE; + reset_quick_slots(); + string[] loadedQuickSlots = get_string_list_or_split(saveData, "equipment_quick_slots"); + uint slot_count = loadedQuickSlots.length(); + if (slot_count > quick_slots.length()) slot_count = quick_slots.length(); + for (uint i = 0; i < slot_count; i++) { + int slot_value = parse_int(loadedQuickSlots[i]); + if (slot_value >= EQUIP_NONE && slot_value <= EQUIP_POUCH) { + quick_slots[i] = slot_value; + } + } + update_max_health_from_equipment(); current_hour = int(get_number(saveData, "time_current_hour", 8)); current_day = int(get_number(saveData, "time_current_day", 1)); @@ -389,6 +833,25 @@ bool load_game_state() { area_expanded_today = get_bool(saveData, "time_area_expanded_today", false); invasion_active = get_bool(saveData, "time_invasion_active", false); invasion_start_hour = int(get_number(saveData, "time_invasion_start_hour", -1)); + invasion_chance = int(get_number(saveData, "time_invasion_chance", 25)); + 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)); + if (invasion_chance < 0) invasion_chance = 0; + if (invasion_chance > 100) invasion_chance = 100; + if (invasion_scheduled_hour < -1) invasion_scheduled_hour = -1; + if (invasion_scheduled_hour > 23) invasion_scheduled_hour = -1; + quest_roll_done_today = get_bool(saveData, "quest_roll_done_today", false); + + quest_queue.resize(0); + string[] loadedQuests = get_string_list_or_split(saveData, "quest_queue"); + for (uint i = 0; i < loadedQuests.length(); i++) { + int quest_type = parse_int(loadedQuests[i]); + if (quest_type >= 0 && quest_type < QUEST_TYPE_COUNT) { + quest_queue.insert_last(quest_type); + if (quest_queue.length() >= QUEST_MAX_ACTIVE) break; + } + } if (current_hour < 0) current_hour = 0; if (current_hour > 23) current_hour = 23; @@ -397,7 +860,7 @@ bool load_game_state() { is_daytime = (current_hour >= 6 && current_hour < 19); hour_timer.restart(); - string[] treeData = get_string_list(saveData, "trees_data"); + string[] treeData = get_string_list_or_split(saveData, "trees_data"); for (uint i = 0; i < treeData.length(); i++) { string[]@ parts = treeData[i].split("|"); if (parts.length() < 8) continue; @@ -415,7 +878,7 @@ bool load_game_state() { trees.insert_last(tree); } - string[] snareData = get_string_list(saveData, "snares_data"); + string[] snareData = get_string_list_or_split(saveData, "snares_data"); for (uint i = 0; i < snareData.length(); i++) { string[]@ parts = snareData[i].split("|"); if (parts.length() < 6) continue; @@ -431,7 +894,7 @@ bool load_game_state() { world_snares.insert_last(snare); } - string[] fireData = get_string_list(saveData, "fires_data"); + string[] fireData = get_string_list_or_split(saveData, "fires_data"); for (uint i = 0; i < fireData.length(); i++) { string[]@ parts = fireData[i].split("|"); if (parts.length() < 3) continue; @@ -444,17 +907,37 @@ bool load_game_state() { world_fires.insert_last(fire); } - string[] firepitPositions = get_string_list(saveData, "firepits_positions"); + string[] firepitPositions = get_string_list_or_split(saveData, "firepits_positions"); for (uint i = 0; i < firepitPositions.length(); i++) { add_world_firepit(parse_int(firepitPositions[i])); } - string[] herbPositions = get_string_list(saveData, "herb_gardens_positions"); + string[] herbPositions = get_string_list_or_split(saveData, "herb_gardens_positions"); for (uint i = 0; i < herbPositions.length(); i++) { add_world_herb_garden(parse_int(herbPositions[i])); } - string[] streamData = get_string_list(saveData, "streams_data"); + string[] storagePositions = get_string_list_or_split(saveData, "storages_positions"); + for (uint i = 0; i < storagePositions.length(); i++) { + add_world_storage(parse_int(storagePositions[i])); + } + + string[] pasturePositions = get_string_list_or_split(saveData, "pastures_positions"); + for (uint i = 0; i < pasturePositions.length(); i++) { + add_world_pasture(parse_int(pasturePositions[i])); + } + + string[] stablePositions = get_string_list_or_split(saveData, "stables_positions"); + for (uint i = 0; i < stablePositions.length(); i++) { + add_world_stable(parse_int(stablePositions[i])); + } + + string[] altarPositions = get_string_list_or_split(saveData, "altars_positions"); + for (uint i = 0; i < altarPositions.length(); i++) { + add_world_altar(parse_int(altarPositions[i])); + } + + string[] streamData = get_string_list_or_split(saveData, "streams_data"); for (uint i = 0; i < streamData.length(); i++) { string[]@ parts = streamData[i].split("|"); if (parts.length() < 2) continue; @@ -465,6 +948,40 @@ bool load_game_state() { add_world_stream(startPos, width); } + string[] banditData = get_string_list_or_split(saveData, "bandits_data"); + for (uint i = 0; i < banditData.length(); i++) { + string[]@ parts = banditData[i].split("|"); + if (parts.length() < 6) continue; + + int pos = parse_int(parts[0]); + int health = parse_int(parts[1]); + string weapon = parts[2]; + string state = parts[3]; + int wander_dir = parse_int(parts[4]); + int move_int = parse_int(parts[5]); + + // Create bandit with dummy expansion area (position will be overridden) + Bandit@ b = Bandit(pos, pos, pos); + 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.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.alert_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]; + b.next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY); + + bandits.insert_last(b); + } + update_ambience(true); return true; } diff --git a/src/time_system.nvgt b/src/time_system.nvgt index 624d786..411de9a 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -12,11 +12,23 @@ bool is_daytime = true; bool sun_setting_warned = false; bool sunrise_warned = false; +// Crossfade state +bool crossfade_active = false; +bool crossfade_to_night = false; // true = fading to night, false = fading to day +timer crossfade_timer; +const int CROSSFADE_DURATION = 60000; // 1 minute (1 game hour) +const float CROSSFADE_MIN_VOLUME = -40.0; // dB, effectively silent but not extreme +const float CROSSFADE_MAX_VOLUME = 0.0; // dB, full volume + // Expansion and invasion tracking bool area_expanded_today = false; bool invasion_active = false; int invasion_start_hour = -1; string[] expanded_terrain_types; +int invasion_chance = 25; +bool invasion_triggered_today = false; +bool invasion_roll_done_today = false; +int invasion_scheduled_hour = -1; void init_time() { current_hour = 8; @@ -25,23 +37,29 @@ void init_time() { is_daytime = true; sun_setting_warned = false; sunrise_warned = false; + crossfade_active = false; + crossfade_to_night = false; area_expanded_today = false; invasion_active = false; invasion_start_hour = -1; + invasion_chance = 25; + invasion_triggered_today = false; + invasion_roll_done_today = false; + invasion_scheduled_hour = -1; update_ambience(true); // Force start } void expand_area() { - if (expanded_area_start != -1) { - return; // Already expanded - } - // Play invasion sound p.play_stationary("sounds/enemies/invasion.ogg", false); // Calculate new area - expanded_area_start = MAP_SIZE; - expanded_area_end = MAP_SIZE + EXPANSION_SIZE - 1; + int new_start = MAP_SIZE; + int new_end = MAP_SIZE + EXPANSION_SIZE - 1; + if (expanded_area_start == -1) { + expanded_area_start = new_start; + } + expanded_area_end = new_end; MAP_SIZE += EXPANSION_SIZE; // Generate a single terrain type for the entire new area @@ -55,9 +73,8 @@ void expand_area() { terrain_type = "snow"; } - expanded_terrain_types.resize(EXPANSION_SIZE); for (int i = 0; i < EXPANSION_SIZE; i++) { - expanded_terrain_types[i] = terrain_type; + expanded_terrain_types.insert_last(terrain_type); } // Place exactly one feature: either a stream or a tree @@ -66,7 +83,7 @@ void expand_area() { if (place_stream) { int stream_width = random(1, 5); int stream_start = random(0, EXPANSION_SIZE - stream_width); - int actual_start = expanded_area_start + stream_start; + int actual_start = new_start + stream_start; add_world_stream(actual_start, stream_width); string width_desc = "very small"; @@ -77,7 +94,7 @@ void expand_area() { notify("A " + width_desc + " stream flows through the new area at x " + actual_start + "."); } else { - int tree_pos = random(expanded_area_start, expanded_area_end); + int tree_pos = random(new_start, new_end); Tree@ t = Tree(tree_pos); trees.insert_last(t); } @@ -87,16 +104,97 @@ void expand_area() { } void start_invasion() { + expand_area(); invasion_active = true; invasion_start_hour = current_hour; notify("Bandits are invading from the new area!"); } +void update_invasion_chance_for_new_day() { + if (current_day == 2) { + invasion_chance = 100; + return; + } + if (current_day > 2) { + if (invasion_triggered_today) { + invasion_chance = 25; + } else { + invasion_chance += 25; + if (invasion_chance > 100) invasion_chance = 100; + } + } +} + +int get_random_invasion_hour(int min_hour) { + if (min_hour < 6) min_hour = 6; + if (min_hour > 11) return -1; + return random(min_hour, 11); +} + +void schedule_invasion() { + if (invasion_scheduled_hour != -1) return; + int hour = get_random_invasion_hour(current_hour); + if (hour == -1) return; + invasion_scheduled_hour = hour; +} + +void check_scheduled_invasion() { + if (invasion_active || invasion_triggered_today) return; + if (invasion_scheduled_hour == -1) return; + if (current_hour == invasion_scheduled_hour) { + invasion_scheduled_hour = -1; + invasion_triggered_today = true; + start_invasion(); + } else if (current_hour > 11) { + invasion_scheduled_hour = -1; + } +} + +void attempt_daily_invasion() { + if (current_day < 2) return; + if (invasion_triggered_today || invasion_active) return; + if (invasion_roll_done_today) return; + invasion_roll_done_today = true; + + int roll = random(1, 100); + if (roll > invasion_chance) return; + + schedule_invasion(); + check_scheduled_invasion(); +} + +void attempt_resident_recruitment() { + if (barricade_health <= 0) { + return; + } + + int chance = random(25, 35); + int roll = random(1, 100); + if (roll > chance) { + return; + } + + int added = random(1, 3); + residents_count += added; + string join_message = (added == 1) ? "A survivor joins your base." : "" + added + " survivors join your base."; + notify(join_message); +} + void end_invasion() { invasion_active = false; invasion_start_hour = -1; - clear_bandits(); + transition_bandits_to_wandering(); notify("The bandit invasion has ended."); + attempt_resident_recruitment(); +} + +void transition_bandits_to_wandering() { + for (uint i = 0; i < bandits.length(); i++) { + bandits[i].behavior_state = "wandering"; + bandits[i].wander_direction = random(-1, 1); + bandits[i].wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); + bandits[i].wander_direction_timer.restart(); + } } void check_invasion_status() { @@ -114,18 +212,62 @@ void check_invasion_status() { } void manage_bandits_during_invasion() { - if (!invasion_active) return; - if (expanded_area_start == -1) return; - - // Bandits only appear during daytime (6 AM to 7 PM) + // Clear ALL bandits at nighttime (undead eat them) if (!is_daytime) { clear_bandits(); return; } - // Maintain BANDIT_MAX_COUNT bandits during invasion - while (bandits.length() < BANDIT_MAX_COUNT) { - spawn_bandit(expanded_area_start, expanded_area_end); + // During daytime: if invasion is active, maintain bandit count + if (invasion_active && expanded_area_start != -1) { + while (bandits.length() < BANDIT_MAX_COUNT) { + spawn_bandit(expanded_area_start, expanded_area_end); + } + } + // If invasion not active, wandering bandits persist during daytime +} + +void update_blessings() { + if (blessing_speed_active && blessing_speed_timer.elapsed >= BLESSING_SPEED_DURATION) { + blessing_speed_active = false; + update_max_health_from_equipment(); + screen_reader_speak("The speed blessing fades.", true); + } +} + +void attempt_blessing() { + if (favor < 1.0) return; + int roll = random(1, 100); + if (roll > BLESSING_TRIGGER_CHANCE) return; + + int[] options; + if (player_health < max_health) options.insert_last(0); + if (!blessing_speed_active) options.insert_last(1); + if (barricade_health < BARRICADE_MAX_HEALTH) options.insert_last(2); + if (options.length() == 0) return; + + int choice = options[random(0, options.length() - 1)]; + favor -= 1.0; + if (favor < 0) favor = 0; + + if (choice == 0) { + int before = player_health; + player_health += BLESSING_HEAL_AMOUNT; + if (player_health > max_health) player_health = max_health; + int healed = player_health - before; + string bonus = (healed > 0) ? "You feel restored. +" + healed + " health." : "You feel restored."; + notify("The gods' favor shines upon you. " + bonus); + } else if (choice == 1) { + blessing_speed_active = true; + blessing_speed_timer.restart(); + update_max_health_from_equipment(); + notify("The gods' favor shines upon you. You feel swift for a while."); + } else if (choice == 2) { + int gained = add_barricade_health(BLESSING_BARRICADE_REPAIR); + string bonus = (gained > 0) + ? "A divine force repairs the barricade. +" + gained + " health." + : "A divine force surrounds the barricade."; + notify("The gods' favor shines upon you. " + bonus); } } @@ -137,6 +279,12 @@ void update_time() { current_hour = 0; current_day++; area_expanded_today = false; // Reset for new day + update_invasion_chance_for_new_day(); + invasion_triggered_today = false; + invasion_roll_done_today = false; + invasion_scheduled_hour = -1; + quest_roll_done_today = false; + consume_food_for_residents(); } if (current_hour == 18 && !sun_setting_warned) { @@ -152,24 +300,32 @@ void update_time() { sunrise_warned = false; } - // Check for area expansion (day 2+, daytime morning before noon, not yet expanded today) - if (current_day >= 2 && current_hour >= 6 && current_hour < 12 && !area_expanded_today && expanded_area_start == -1) { - int roll = random(1, 100); - if (roll <= EXPANSION_CHANCE) { - expand_area(); - // Start invasion immediately after expansion (morning, during daytime) - start_invasion(); - } - } - // Check invasion status check_invasion_status(); check_ambience_transition(); + // TODO: add resident defense using stored weapons once storage exists. + if (is_daytime && residents_count > 0 && barricade_health < BARRICADE_MAX_HEALTH && current_hour % 4 == 0) { + if (storage_meat > 0) { + int gained = add_barricade_health(residents_count); + if (gained > 0 && x <= BASE_END) { + screen_reader_speak("Residents repaired the barricade. +" + gained + " health.", true); + } + } + } + if (current_hour == 6) { save_game_state(); } + + if (current_hour == 6) { + attempt_daily_invasion(); + attempt_daily_quest(); + } + keep_base_fires_fed(); + check_scheduled_invasion(); + attempt_blessing(); } // Manage bandits during active invasion @@ -199,40 +355,108 @@ string get_time_string() { } void check_ambience_transition() { - // Definition of Day: 6 AM to 7 PM (19:00) ? - // Let's say Day is 6 (6AM) to 19 (7PM). Night is 20 (8PM) to 5 (5AM). - // Or simpler: 6 to 18 (6PM). - - bool now_day = (current_hour >= 6 && current_hour < 19); - - if (now_day != is_daytime) { - is_daytime = now_day; - update_ambience(false); + // Day is 6 (6AM) to 18 (6PM inclusive, so transition starts at hour 18) + // Night is 19 (7PM) to 5 (5AM inclusive, so transition starts at hour 5) + // Crossfade begins at hour 18 (sunset) and hour 5 (sunrise) + + // Start crossfade to night at hour 18 + if (current_hour == 18 && is_daytime && !crossfade_active) { + start_crossfade(true); // Fade to night + } + // Start crossfade to day at hour 5 + else if (current_hour == 5 && !is_daytime && !crossfade_active) { + start_crossfade(false); // Fade to day + } +} + +void start_crossfade(bool to_night) { + crossfade_active = true; + crossfade_to_night = to_night; + crossfade_timer.restart(); + + // Start the incoming sound at minimum volume (silent) + if (to_night) { + // Starting night sound + if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) { + 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)) { + 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); + } +} + +void update_crossfade() { + if (!crossfade_active) return; + + float progress = float(crossfade_timer.elapsed) / float(CROSSFADE_DURATION); + if (progress > 1.0) progress = 1.0; + + // Volume interpolation: fade out goes 0 -> -40, fade in goes -40 -> 0 + float volume_range = CROSSFADE_MAX_VOLUME - CROSSFADE_MIN_VOLUME; // 40 dB range + float fade_out_vol = CROSSFADE_MAX_VOLUME - (volume_range * progress); // 0 -> -40 + float fade_in_vol = CROSSFADE_MIN_VOLUME + (volume_range * progress); // -40 -> 0 + + if (crossfade_to_night) { + // Fading day out, night in + if (day_sound_handle != -1) p.update_sound_start_values(day_sound_handle, 0.0, fade_out_vol, 1.0); + if (night_sound_handle != -1) p.update_sound_start_values(night_sound_handle, 0.0, fade_in_vol, 1.0); + } else { + // Fading night out, day in + if (night_sound_handle != -1) p.update_sound_start_values(night_sound_handle, 0.0, fade_out_vol, 1.0); + if (day_sound_handle != -1) p.update_sound_start_values(day_sound_handle, 0.0, fade_in_vol, 1.0); + } + + // Complete the crossfade + if (progress >= 1.0) { + complete_crossfade(); + } +} + +void complete_crossfade() { + crossfade_active = false; + + if (crossfade_to_night) { + // Destroy day sound, ensure night is at full volume + if (day_sound_handle != -1) { + p.destroy_sound(day_sound_handle); + day_sound_handle = -1; + } + if (night_sound_handle != -1) p.update_sound_start_values(night_sound_handle, 0.0, 0.0, 1.0); + is_daytime = false; + } else { + // Destroy night sound, ensure day is at full volume + if (night_sound_handle != -1) { + p.destroy_sound(night_sound_handle); + night_sound_handle = -1; + } + if (day_sound_handle != -1) p.update_sound_start_values(day_sound_handle, 0.0, 0.0, 1.0); + is_daytime = true; } } void update_ambience(bool force_restart) { if (is_daytime) { - // Transition to Day + // Start day ambience if (night_sound_handle != -1) { p.destroy_sound(night_sound_handle); night_sound_handle = -1; } if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) { - // Play looped, stationary (or relative to player x?) - // Usually ambience is 2D/Global. play_stationary_extended allows looping. - // Or play_1d at player position if we want panning? - // "sounds/nature/day.ogg" - day_sound_handle = p.play_stationary("sounds/nature/day.ogg", true); + day_sound_handle = p.play_stationary("sounds/nature/day.ogg", true); } } else { - // Transition to Night + // Start night ambience if (day_sound_handle != -1) { p.destroy_sound(day_sound_handle); day_sound_handle = -1; } if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) { - night_sound_handle = p.play_stationary("sounds/nature/night.ogg", true); + night_sound_handle = p.play_stationary("sounds/nature/night.ogg", true); } } } diff --git a/src/ui.nvgt b/src/ui.nvgt new file mode 100644 index 0000000..c9df2f8 --- /dev/null +++ b/src/ui.nvgt @@ -0,0 +1,17 @@ +// UI helpers +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"); + return result; +} + +int ui_question(const string title, const string prompt) { + int result = virtual_question(title, prompt); + show_window("Draugnorak"); + return result; +} + +void ui_info_box(const string title, const string heading, const string message) { + virtual_info_box(title, heading, message); + show_window("Draugnorak"); +} diff --git a/src/world_state.nvgt b/src/world_state.nvgt index f90e55f..0a7d874 100644 --- a/src/world_state.nvgt +++ b/src/world_state.nvgt @@ -5,6 +5,7 @@ string[] small_game_types = {"rabbit", "squirrel", "raccoon", "opossum", "ground int barricade_health = 0; bool barricade_initialized = false; +int residents_count = 0; string[] zombie_sounds = {"sounds/enemies/zombie1.ogg"}; string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"}; @@ -13,6 +14,7 @@ class Zombie { int position; int health; string voice_sound; + int sound_handle; timer move_timer; timer groan_timer; timer attack_timer; @@ -23,6 +25,7 @@ class Zombie { health = ZOMBIE_HEALTH; int sound_index = random(0, zombie_sounds.length() - 1); voice_sound = zombie_sounds[sound_index]; + sound_handle = -1; move_timer.restart(); groan_timer.restart(); attack_timer.restart(); @@ -42,6 +45,12 @@ class Bandit { int next_alert_delay; int move_interval; + // Wandering behavior properties + string behavior_state; // "aggressive" or "wandering" + int wander_direction; // -1, 0, or 1 + timer wander_direction_timer; + int wander_direction_change_interval; + Bandit(int pos, int expansion_start, int expansion_end) { // Spawn somewhere in the expanded area position = random(expansion_start, expansion_end); @@ -61,6 +70,12 @@ class Bandit { alert_timer.restart(); attack_timer.restart(); next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY); + + // Initialize wandering behavior (start aggressive during invasion) + behavior_state = "aggressive"; + wander_direction = 0; + wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); + wander_direction_timer.restart(); } } Bandit@[] bandits; @@ -122,10 +137,9 @@ class WorldSnare { int roll = random(1, 100); if (roll <= escape_chance) { // Animal escaped! - has_catch = false; - notify("A " + catch_type + " escaped from your snare at " + position + "!"); - catch_type = ""; - catch_chance = 5; + notify("A " + catch_type + " escaped from your snare at x " + position + " y 0!"); + remove_snare_at(position); + return; } } else { // Trying to catch small game @@ -243,6 +257,42 @@ class WorldHerbGarden { } WorldHerbGarden@[] world_herb_gardens; +class WorldStorage { + int position; + + WorldStorage(int pos) { + position = pos; + } +} +WorldStorage@[] world_storages; + +class WorldPasture { + int position; + + WorldPasture(int pos) { + position = pos; + } +} +WorldPasture@[] world_pastures; + +class WorldStable { + int position; + + WorldStable(int pos) { + position = pos; + } +} +WorldStable@[] world_stables; + +class WorldAltar { + int position; + + WorldAltar(int pos) { + position = pos; + } +} +WorldAltar@[] world_altars; + class WorldStream { int start_position; int end_position; @@ -269,10 +319,14 @@ class WorldStream { void update() { int center = get_center_position(); - // Play stream sound within 3 tiles distance from center - if (abs(x - center) <= 3) { + // Play stream sound within range of center + if (abs(x - center) <= STREAM_SOUND_RANGE) { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { sound_handle = p.play_1d("sounds/terrain/stream.ogg", x, center, true); + if (sound_handle != -1) { + p.update_sound_positioning_values(sound_handle, -1.0, STREAM_SOUND_VOLUME_STEP, true); + p.update_sound_range_1d(sound_handle, STREAM_SOUND_RANGE, STREAM_SOUND_RANGE); + } } } else { if (sound_handle != -1) { @@ -306,6 +360,62 @@ void add_world_firepit(int pos) { world_firepits.insert_last(fp); } +void add_world_storage(int pos) { + WorldStorage@ s = WorldStorage(pos); + world_storages.insert_last(s); +} + +void add_world_pasture(int pos) { + WorldPasture@ p = WorldPasture(pos); + world_pastures.insert_last(p); +} + +void add_world_stable(int pos) { + WorldStable@ s = WorldStable(pos); + world_stables.insert_last(s); +} + +void add_world_altar(int pos) { + WorldAltar@ a = WorldAltar(pos); + world_altars.insert_last(a); +} + +WorldStorage@ get_storage_at(int pos) { + for (uint i = 0; i < world_storages.length(); i++) { + if (world_storages[i].position == pos) { + return @world_storages[i]; + } + } + return null; +} + +WorldPasture@ get_pasture_at(int pos) { + for (uint i = 0; i < world_pastures.length(); i++) { + if (world_pastures[i].position == pos) { + return @world_pastures[i]; + } + } + return null; +} + +WorldStable@ get_stable_at(int pos) { + for (uint i = 0; i < world_stables.length(); i++) { + if (world_stables[i].position == pos) { + return @world_stables[i]; + } + } + return null; +} + +WorldAltar@ get_altar_at(int pos) { + for (uint i = 0; i < world_altars.length(); i++) { + if (world_altars[i].position == pos) { + return @world_altars[i]; + } + } + return null; +} + void update_world_objects() { for (uint i = 0; i < world_snares.length(); i++) { world_snares[i].update(); @@ -352,7 +462,7 @@ void check_snare_collision(int player_x) { } void update_snares() { - for (uint i = 0; i < world_snares.length(); i++) { + for (int i = int(world_snares.length()) - 1; i >= 0; i--) { world_snares[i].update(); } } @@ -461,6 +571,12 @@ int add_barricade_health(int amount) { void clear_zombies() { if (zombies.length() == 0) return; + for (uint i = 0; i < zombies.length(); i++) { + if (zombies[i].sound_handle != -1) { + p.destroy_sound(zombies[i].sound_handle); + zombies[i].sound_handle = -1; + } + } zombies.resize(0); } @@ -488,7 +604,7 @@ void spawn_zombie() { Zombie@ z = Zombie(spawn_x); zombies.insert_last(z); - play_1d_with_volume_step(z.voice_sound, x, spawn_x, false, ZOMBIE_SOUND_VOLUME_STEP); + z.sound_handle = play_1d_with_volume_step(z.voice_sound, x, spawn_x, false, ZOMBIE_SOUND_VOLUME_STEP); } void try_attack_barricade(Zombie@ zombie) { @@ -546,7 +662,7 @@ void update_zombie(Zombie@ zombie) { if (zombie.groan_timer.elapsed > zombie.next_groan_delay) { zombie.groan_timer.restart(); zombie.next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY); - play_1d_with_volume_step(zombie.voice_sound, x, zombie.position, false, ZOMBIE_SOUND_VOLUME_STEP); + zombie.sound_handle = play_1d_with_volume_step(zombie.voice_sound, x, zombie.position, false, ZOMBIE_SOUND_VOLUME_STEP); } if (try_attack_player(zombie)) { @@ -607,9 +723,12 @@ bool damage_zombie_at(int pos, int damage) { if (zombies[i].position == pos) { zombies[i].health -= damage; if (zombies[i].health <= 0) { + if (zombies[i].sound_handle != -1) { + p.destroy_sound(zombies[i].sound_handle); + zombies[i].sound_handle = -1; + } play_1d_with_volume_step("sounds/enemies/enemy_falls.ogg", x, pos, false, ZOMBIE_SOUND_VOLUME_STEP); zombies.remove_at(i); - } else { } return true; } @@ -766,37 +885,79 @@ void update_bandit(Bandit@ bandit) { return; } - // Move toward player - int direction = 0; - if (x > BASE_END) { - // Player is outside base, move toward them - if (x > bandit.position) { - direction = 1; - } else if (x < bandit.position) { - direction = -1; + // State-based behavior + if (bandit.behavior_state == "wandering") { + // Check if player is within detection radius + int distance = abs(bandit.position - x); + if (distance <= BANDIT_DETECTION_RADIUS) { + // Player detected! Switch to aggressive + bandit.behavior_state = "aggressive"; } else { + // Continue wandering + if (bandit.wander_direction_timer.elapsed > bandit.wander_direction_change_interval) { + // Time to change direction + bandit.wander_direction = random(-1, 1); + bandit.wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); + bandit.wander_direction_timer.restart(); + } + + // Move in wander direction (if not 0) + if (bandit.wander_direction != 0) { + int target_x = bandit.position + bandit.wander_direction; + + // Check bounds + if (target_x >= 0 && target_x < MAP_SIZE) { + // Don't wander into base if barricade is up + if (target_x <= BASE_END && barricade_health > 0) { + // Change direction instead + bandit.wander_direction = -bandit.wander_direction; + } else { + bandit.position = target_x; + play_positional_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); + } + } else { + // Hit map boundary, reverse direction + bandit.wander_direction = -bandit.wander_direction; + } + } return; } - } else { - // Player is in base, move toward base edge - if (bandit.position > BASE_END + 1) { - direction = -1; + } + + // Aggressive behavior (original logic) + if (bandit.behavior_state == "aggressive") { + // Move toward player + int direction = 0; + if (x > BASE_END) { + // Player is outside base, move toward them + if (x > bandit.position) { + direction = 1; + } else if (x < bandit.position) { + direction = -1; + } else { + return; + } } else { - return; // Already at base edge + // Player is in base, move toward base edge + if (bandit.position > BASE_END + 1) { + direction = -1; + } else { + return; // Already at base edge + } } + + int target_x = bandit.position + direction; + if (target_x < 0 || target_x >= MAP_SIZE) return; + + // Don't enter base if barricade is up + if (target_x <= BASE_END && barricade_health > 0) { + try_attack_barricade_bandit(bandit); + return; + } + + bandit.position = target_x; + play_positional_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); } - - int target_x = bandit.position + direction; - if (target_x < 0 || target_x >= MAP_SIZE) return; - - // Don't enter base if barricade is up - if (target_x <= BASE_END && barricade_health > 0) { - try_attack_barricade_bandit(bandit); - return; - } - - bandit.position = target_x; - play_positional_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); } void update_bandits() {