From 4caa5caefb9ad6071193dc4097150db791b6bdc7 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 20 Jan 2026 12:30:26 -0500 Subject: [PATCH] Weather system added, mostly decoration. Some tweaks to residents. Moved altar to its own menu, s for sacrifice. You can no longer burn incense outside the base. --- sounds/game/game_falls.ogg | 3 + sounds/game/goose.ogg | 3 + sounds/items/item.ogg | 3 + sounds/items/miscellaneous.ogg | 4 +- sounds/nature/rain.ogg | 3 + sounds/nature/thunder_high.ogg | 3 + sounds/nature/thunder_low.ogg | 3 + sounds/nature/thunder_medium.ogg | 3 + sounds/nature/wind_high.ogg | 3 + sounds/nature/wind_low.ogg | 3 + sounds/nature/wind_medium.ogg | 3 + src/base_system.nvgt | 116 ++++++- src/combat.nvgt | 19 +- src/constants.nvgt | 49 ++- src/crafting.nvgt | 101 ++++-- src/creature_audio.nvgt | 7 +- src/creature_template.nvgt | 327 +++++++++++++++++++ src/environment.nvgt | 71 ++++- src/flying_creature_template.nvgt | 71 +++++ src/inventory_items.nvgt | 25 ++ src/inventory_menus.nvgt | 62 +++- src/notify.nvgt | 39 ++- src/player.nvgt | 2 + src/save_system.nvgt | 56 ++++ src/time_system.nvgt | 67 +++- src/ui.nvgt | 3 +- src/weather.nvgt | 492 +++++++++++++++++++++++++++++ src/world_state.nvgt | 504 +++++++++++++++++++++++++++++- 28 files changed, 1973 insertions(+), 72 deletions(-) create mode 100644 sounds/game/game_falls.ogg create mode 100644 sounds/game/goose.ogg create mode 100644 sounds/items/item.ogg create mode 100644 sounds/nature/rain.ogg create mode 100644 sounds/nature/thunder_high.ogg create mode 100644 sounds/nature/thunder_low.ogg create mode 100644 sounds/nature/thunder_medium.ogg create mode 100644 sounds/nature/wind_high.ogg create mode 100644 sounds/nature/wind_low.ogg create mode 100644 sounds/nature/wind_medium.ogg create mode 100644 src/creature_template.nvgt create mode 100644 src/flying_creature_template.nvgt create mode 100644 src/weather.nvgt diff --git a/sounds/game/game_falls.ogg b/sounds/game/game_falls.ogg new file mode 100644 index 0000000..a9adc9e --- /dev/null +++ b/sounds/game/game_falls.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b60dea3c4c473bb17e0be0d56ae48a31497d927bea927c39896848c2bd55bc0 +size 7510 diff --git a/sounds/game/goose.ogg b/sounds/game/goose.ogg new file mode 100644 index 0000000..c371579 --- /dev/null +++ b/sounds/game/goose.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f190468f614df17365ba9b553a6394069665148cb58b9c00540919a1fb11f103 +size 40782 diff --git a/sounds/items/item.ogg b/sounds/items/item.ogg new file mode 100644 index 0000000..42245cd --- /dev/null +++ b/sounds/items/item.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ba759a862771a53dcc07befe8fcbac266f627614955e1c2afa28f62d3e1d25e +size 7473 diff --git a/sounds/items/miscellaneous.ogg b/sounds/items/miscellaneous.ogg index b633f40..6dc71f4 100644 --- a/sounds/items/miscellaneous.ogg +++ b/sounds/items/miscellaneous.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a66b30f348f842d5be507df20b4d538964af7fd3d754668527f522ead37fbbdc -size 4970 +oid sha256:7e7a557ff8ad9bc037e8515f0df465b1f0cf4fc025a3836f2e3ba0bcf743ac71 +size 4917 diff --git a/sounds/nature/rain.ogg b/sounds/nature/rain.ogg new file mode 100644 index 0000000..de46a6b --- /dev/null +++ b/sounds/nature/rain.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e29d552ecd95c9e3906dc599ab7b7dcc203766079915af8ca373104f75c4425f +size 426321 diff --git a/sounds/nature/thunder_high.ogg b/sounds/nature/thunder_high.ogg new file mode 100644 index 0000000..30ddce5 --- /dev/null +++ b/sounds/nature/thunder_high.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:631ed74851bdc191e27d7197b04736b105017c35240cb598937be1d9fb5f6a98 +size 66970 diff --git a/sounds/nature/thunder_low.ogg b/sounds/nature/thunder_low.ogg new file mode 100644 index 0000000..b245a59 --- /dev/null +++ b/sounds/nature/thunder_low.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9a3599ad2ba1b0e4cd9108b3a5e431b8899baaa238a6ffae36585a8ee1d477e +size 40033 diff --git a/sounds/nature/thunder_medium.ogg b/sounds/nature/thunder_medium.ogg new file mode 100644 index 0000000..062d649 --- /dev/null +++ b/sounds/nature/thunder_medium.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a81505b7032d7f220d8f91bd5763e67503a3917ac6c06fa431559e2ea7f87442 +size 63917 diff --git a/sounds/nature/wind_high.ogg b/sounds/nature/wind_high.ogg new file mode 100644 index 0000000..126da73 --- /dev/null +++ b/sounds/nature/wind_high.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25c23e6fb9de4ad7b127ec000c5fb73bb7e2d9771b95aebb7fedfbcc2f80388b +size 144107 diff --git a/sounds/nature/wind_low.ogg b/sounds/nature/wind_low.ogg new file mode 100644 index 0000000..c472993 --- /dev/null +++ b/sounds/nature/wind_low.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28b38d69b49f4cfa3cb2408bc685dcde9b06e20d306546b3cbb0409d1325f45a +size 83447 diff --git a/sounds/nature/wind_medium.ogg b/sounds/nature/wind_medium.ogg new file mode 100644 index 0000000..f2a41e5 --- /dev/null +++ b/sounds/nature/wind_medium.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf284d74051f17dfa65c9e936879db3f3811ef1a2126fca6295e3ad4d46affd2 +size 74668 diff --git a/src/base_system.nvgt b/src/base_system.nvgt index a401899..bb64dfc 100644 --- a/src/base_system.nvgt +++ b/src/base_system.nvgt @@ -76,16 +76,78 @@ int perform_resident_defense() { int damage = 0; if (useSpear && storage_spears > 0) { damage = RESIDENT_SPEAR_DAMAGE; - play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, 3.0); + // Weapons don't get consumed on use - they break via daily breakage check + // Just play the sound + play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP); } else if (storage_slings > 0 && storage_stones > 0) { damage = random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX); + // Slings use stones as ammo, so consume a stone storage_stones--; - play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, BASE_END + 1, false, 3.0); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP); } return damage; } +// Proactive resident sling defense +timer resident_sling_timer; +const int RESIDENT_SLING_COOLDOWN = 4000; // 4 seconds between shots + +void attempt_resident_sling_defense() { + // Only if residents exist and have slings with stones + if (residents_count <= 0) return; + if (storage_slings <= 0 || storage_stones <= 0) return; + + // Cooldown between shots + if (resident_sling_timer.elapsed < RESIDENT_SLING_COOLDOWN) return; + + // Find nearest enemy within sling range + int nearestDistance = SLING_RANGE + 1; + int targetPos = -1; + bool targetIsBandit = false; + + int sling_origin = BASE_END; + + // Check zombies + for (uint i = 0; i < zombies.length(); i++) { + int dist = abs(zombies[i].position - sling_origin); + if (dist > 0 && dist <= SLING_RANGE && dist < nearestDistance) { + nearestDistance = dist; + targetPos = zombies[i].position; + targetIsBandit = false; + } + } + + // Check bandits + for (uint i = 0; i < bandits.length(); i++) { + int dist = abs(bandits[i].position - sling_origin); + if (dist > 0 && dist <= SLING_RANGE && dist < nearestDistance) { + nearestDistance = dist; + targetPos = bandits[i].position; + targetIsBandit = true; + } + } + + // No targets in range + if (targetPos == -1) return; + + // Shoot! + resident_sling_timer.restart(); + storage_stones--; + + int damage = random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, targetPos, false, RESIDENT_DEFENSE_VOLUME_STEP); + + if (targetIsBandit) { + damage_bandit_at(targetPos, damage); + } else { + damage_zombie_at(targetPos, damage); + } + + // Play hit sound on enemy + play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, targetPos, ZOMBIE_SOUND_VOLUME_STEP); +} + void process_daily_weapon_breakage() { if (residents_count <= 0) return; @@ -149,3 +211,53 @@ void process_daily_weapon_breakage() { notify(msg); } } + +// Resident resource collection +const int RESIDENT_COLLECTION_CHANCE = 10; // 10% chance per basket per hour + +void attempt_resident_collection() { + // Only during daytime + if (!is_daytime) return; + + // Need residents + if (residents_count <= 0) return; + + // Need baskets in storage to enable collection + if (storage_reed_baskets <= 0) return; + + // Number of residents who can collect = min(residents, baskets) + int active_collectors = (residents_count < storage_reed_baskets) ? residents_count : storage_reed_baskets; + + // Each active collector has a 10% chance to collect something + for (int i = 0; i < active_collectors; i++) { + if (random(1, 100) > RESIDENT_COLLECTION_CHANCE) continue; + + // Determine what to collect (weighted random) + // Sticks and vines more common, logs and stones less common + int roll = random(1, 100); + string item_name = ""; + + if (roll <= 40) { + // 40% chance - stick + storage_sticks++; + item_name = "stick"; + } else if (roll <= 70) { + // 30% chance - vine + storage_vines++; + item_name = "vine"; + } else if (roll <= 85) { + // 15% chance - stone + storage_stones++; + item_name = "stone"; + } else { + // 15% chance - log + storage_logs++; + item_name = "log"; + } + + // Announce only if player is in base + if (x <= BASE_END) { + screen_reader_speak("Resident added " + item_name + " to storage.", true); + } + } +} diff --git a/src/combat.nvgt b/src/combat.nvgt index 5e619d5..e022a16 100644 --- a/src/combat.nvgt +++ b/src/combat.nvgt @@ -130,6 +130,7 @@ void release_sling_attack(int player_x) { int search_direction = (facing == 1) ? 1 : -1; int target_x = -1; bool hit_bandit = false; + bool hit_flying_creature = false; // Priority: Find nearest enemy (bandit or zombie) first for (int dist = 1; dist <= SLING_RANGE; dist++) { @@ -151,6 +152,14 @@ void release_sling_attack(int player_x) { hit_bandit = false; break; } + + // Then check for flying creature (only if flying) + FlyingCreature@ creature = get_flying_creature_at(check_x); + if (creature != null && creature.state == "flying") { + target_x = check_x; + hit_flying_creature = true; + break; + } } // If no enemy found, check for trees (but don't damage them) @@ -161,7 +170,7 @@ void release_sling_attack(int player_x) { Tree@ tree = get_tree_at(check_x); if (tree != null && !tree.is_chopped) { // Stone hits tree but doesn't damage it - p.play_1d("sounds/weapons/sling_hit.ogg", player_x, check_x, false); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, check_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); screen_reader_speak("Stone hit tree at " + check_x + ".", true); return; } @@ -179,11 +188,15 @@ void release_sling_attack(int player_x) { // Damage the correct enemy type if (hit_bandit) { damage_bandit_at(target_x, damage); - p.play_1d("sounds/weapons/sling_hit.ogg", player_x, target_x, false); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, BANDIT_SOUND_VOLUME_STEP); + } else if (hit_flying_creature) { + damage_flying_creature_at(target_x, damage); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + // Falling sound handled by damage_flying_creature_at } else { damage_zombie_at(target_x, damage); - p.play_1d("sounds/weapons/sling_hit.ogg", player_x, target_x, false); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, ZOMBIE_SOUND_VOLUME_STEP); } } diff --git a/src/constants.nvgt b/src/constants.nvgt index 65a903b..9115107 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -30,7 +30,22 @@ 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; +const int SLING_RANGE = 8; + +// Bow settings (for future implementation) +// Option 1: Longer range, similar damage +// const int BOW_DAMAGE_MIN = 6; +// const int BOW_DAMAGE_MAX = 9; +// const int BOW_RANGE = 12; // 50% more range than sling +// +// Option 2: Much longer range, slightly more damage +// const int BOW_DAMAGE_MIN = 7; +// const int BOW_DAMAGE_MAX = 10; +// const int BOW_RANGE = 15; // Nearly double sling range +// +// Recommendation: Bows should have BOTH more range AND more damage than slings +// to justify the likely higher resource cost and complexity to craft. +// Suggested balance: BOW_RANGE = 12, damage 6-9 (average 7.5 vs sling's 6.5) // Zombie settings const int ZOMBIE_HEALTH = 12; @@ -68,6 +83,11 @@ const int STABLE_STONE_COST = 15; const int STABLE_VINE_COST = 10; const int ALTAR_STONE_COST = 9; const int ALTAR_STICK_COST = 3; +const int INCENSE_STICK_COST = 6; +const int INCENSE_VINE_COST = 2; +const int INCENSE_REED_COST = 1; +const int INCENSE_HOURS_PER_STICK = 4; +const double INCENSE_FAVOR_PER_HOUR = 0.3; // Bandit settings const int BANDIT_HEALTH = 4; @@ -99,7 +119,7 @@ const float SNARE_SOUND_PAN_STEP = 4.0; // Stronger pan for direction const int SNARE_COLLECT_RANGE = 1; const int FIRE_SOUND_RANGE = 3; -const float FIRE_SOUND_VOLUME_STEP = 5.0; // 30 dB over 6 tiles +const float FIRE_SOUND_VOLUME_STEP = 5.0; // 15 dB over 3 tiles (FIRE_SOUND_RANGE) const int FIREPIT_SOUND_RANGE = 5; const float FIREPIT_SOUND_VOLUME_STEP = 6.0; // 30 dB over 5 tiles @@ -108,6 +128,12 @@ const int STREAM_SOUND_RANGE = 7; const float STREAM_SOUND_VOLUME_STEP = 4.3; // 30 dB over 7 tiles const float TREE_SOUND_VOLUME_STEP = 4.0; // Similar to snares for good audibility +const int TREE_SOUND_RANGE = 4; + +const float RESIDENT_DEFENSE_VOLUME_STEP = 3.0; // Default volume for resident counter-attacks +const float PLAYER_WEAPON_SOUND_VOLUME_STEP = 3.0; +const int FLYING_CREATURE_FADE_OUT_DURATION = 1500; // ms +const float FLYING_CREATURE_FADE_OUT_MIN_VOLUME = -40.0; // dB // Mountain configuration const int MOUNTAIN_SIZE = 60; @@ -126,8 +152,25 @@ const int QUEST_STONE_SCORE = 6; const int QUEST_LOG_SCORE = 10; const int QUEST_SKIN_SCORE = 14; -// Resident defense settings +// Resident settings +const int MAX_RESIDENTS = 4; // Max residents per base (+ player = 5 total) const int RESIDENT_WEAPON_BREAK_CHANCE = 10; const int RESIDENT_SPEAR_DAMAGE = 2; const int RESIDENT_SLING_DAMAGE_MIN = 3; const int RESIDENT_SLING_DAMAGE_MAX = 5; + +// Goose settings +const int GOOSE_HEALTH = 1; +const int GOOSE_MOVE_INTERVAL_MIN = 800; // Faster movement +const int GOOSE_MOVE_INTERVAL_MAX = 2000; +const int GOOSE_FLYING_HEIGHT_MIN = 10; +const int GOOSE_FLYING_HEIGHT_MAX = 30; +const float GOOSE_SOUND_VOLUME_STEP = 3.0; +const int GOOSE_FLIGHT_SOUND_DELAY_MIN = 2000; // Honk more often +const int GOOSE_FLIGHT_SOUND_DELAY_MAX = 5000; +const int GOOSE_FALL_SPEED = 100; // ms per foot +const int GOOSE_FLY_AWAY_CHANCE = 0; // Chance out of 1000 per tick to fly away +const int GOOSE_MAX_DIST_FROM_WATER = 4; // How far they can wander from water +const int GOOSE_MAX_COUNT = 3; +const int GOOSE_HOURLY_SPAWN_CHANCE = 35; // Percent chance per hour to spawn a goose +const int GOOSE_SIGHT_RANGE = 0; diff --git a/src/crafting.nvgt b/src/crafting.nvgt index 755a722..04fa4a1 100644 --- a/src/crafting.nvgt +++ b/src/crafting.nvgt @@ -11,12 +11,8 @@ 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); - } + string[] categories = {"Weapons", "Tools", "Materials", "Clothing", "Buildings", "Barricade"}; + int[] category_types = {0, 1, 2, 3, 4, 5}; while(true) { wait(5); @@ -42,10 +38,10 @@ void run_crafting_menu() { 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(); + else if (category == 2) run_materials_menu(); + else if (category == 3) run_clothing_menu(); + else if (category == 4) run_buildings_menu(); + else if (category == 5) run_barricade_menu(); break; } } @@ -99,8 +95,7 @@ void run_tools_menu() { "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]" + "Clay Pot (3 Clay)" }; while(true) { @@ -131,12 +126,47 @@ void run_tools_menu() { 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_materials_menu() { + screen_reader_speak("Materials.", true); + + int selection = 0; + string[] options = { + "Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]", + "Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]" + }; + + 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) butcher_small_game(); + else if (selection == 1) craft_incense(); + break; + } + } +} void run_clothing_menu() { screen_reader_speak("Clothing.", true); @@ -856,6 +886,33 @@ void craft_clay_pot() { } } +void craft_incense() { + if (world_altars.length() == 0) { + screen_reader_speak("You need an altar to craft incense.", true); + return; + } + + string missing = ""; + if (inv_sticks < INCENSE_STICK_COST) missing += INCENSE_STICK_COST + " sticks "; + if (inv_vines < INCENSE_VINE_COST) missing += INCENSE_VINE_COST + " vines "; + if (inv_reeds < INCENSE_REED_COST) missing += INCENSE_REED_COST + " reed "; + + if (missing == "") { + if (inv_incense >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more incense.", true); + return; + } + simulate_crafting(INCENSE_STICK_COST + INCENSE_VINE_COST + INCENSE_REED_COST); + inv_sticks -= INCENSE_STICK_COST; + inv_vines -= INCENSE_VINE_COST; + inv_reeds -= INCENSE_REED_COST; + inv_incense++; + screen_reader_speak("Crafted incense.", true); + } else { + screen_reader_speak("Missing: " + missing, true); + } +} + void butcher_small_game() { string missing = ""; @@ -883,15 +940,23 @@ void butcher_small_game() { } simulate_crafting(1); - // 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); + if (game_type == "goose") { + inv_meat++; + inv_feathers += random(3, 6); + inv_down += random(1, 3); + screen_reader_speak("Butchered goose. Got 1 meat, feathers, and down.", true); + } else { + inv_meat++; + inv_skins++; + screen_reader_speak("Butchered " + game_type + ". Got 1 meat and 1 skin.", true); + } + + // Play sound + p.play_stationary("sounds/items/miscellaneous.ogg", false); } else { screen_reader_speak("Missing: " + missing, true); } diff --git a/src/creature_audio.nvgt b/src/creature_audio.nvgt index 24996fc..f43dde3 100644 --- a/src/creature_audio.nvgt +++ b/src/creature_audio.nvgt @@ -1,11 +1,14 @@ // Unified creature/enemy audio system // Ensures consistent panning and distance behavior for all animated entities // -// USAGE GUIDE: +// *** IMPORTANT: See src/creature_template.nvgt for complete step-by-step guide *** +// The template file has code examples, a checklist, and enforces consistency. +// +// QUICK USAGE GUIDE: // This system provides standardized audio functions for all creatures (zombies, bandits, animals). // Using these functions ensures all creatures sound consistent to the player. // -// When adding new creatures (sheep, cattle, horses, etc): +// When adding new creatures (goblins, sheep, cattle, horses, etc): // 1. Define creature-specific constants in constants.nvgt: // const float SHEEP_SOUND_VOLUME_STEP = 3.0; // const int SHEEP_FOOTSTEP_MAX_DISTANCE = 5; diff --git a/src/creature_template.nvgt b/src/creature_template.nvgt new file mode 100644 index 0000000..16e31f6 --- /dev/null +++ b/src/creature_template.nvgt @@ -0,0 +1,327 @@ +// CREATURE CREATION TEMPLATE +// Use this as a guide when creating new creatures (goblins, animals, etc.) +// +// This template ensures all creatures have consistent audio behavior and required features. +// Copy the sections you need and fill in creature-specific details. + +/* ============================================================================ + STEP 1: Add constants to src/constants.nvgt + ============================================================================ */ + +// Example for "Goblin" creature: +/* +// Goblin Configuration +const int GOBLIN_HEALTH = 15; +const int GOBLIN_DAMAGE_MIN = 2; +const int GOBLIN_DAMAGE_MAX = 4; +const int GOBLIN_MOVE_INTERVAL = 1500; +const int GOBLIN_ATTACK_INTERVAL = 3000; +const int GOBLIN_ALERT_MIN_DELAY = 4000; +const int GOBLIN_ALERT_MAX_DELAY = 8000; +const int GOBLIN_FOOTSTEP_MAX_DISTANCE = 6; +const float GOBLIN_SOUND_VOLUME_STEP = 3.0; // Default creature volume +*/ + +/* ============================================================================ + STEP 2: Define sound arrays in src/world_state.nvgt (near top with zombies/bandits) + ============================================================================ */ + +// Example: +/* +string[] goblin_sounds = { + "sounds/enemies/goblin_cackle1.ogg", + "sounds/enemies/goblin_cackle2.ogg", + "sounds/enemies/goblin_cackle3.ogg" +}; +*/ + +/* ============================================================================ + STEP 3: Create creature class in src/world_state.nvgt + ============================================================================ + + REQUIRED MEMBERS: + - int position (where the creature is on the map) + - int health (creature's HP) + - int sound_handle (CRITICAL: for tracking active voice/alert sounds) + - timer move_timer (for movement intervals) + - timer attack_timer (for attack cooldown) + - timer voice_timer (for periodic sounds - groans/alerts/cackles) + - int next_voice_delay (randomized delay between voice sounds) + - string voice_sound (which sound file to play for voice) + + OPTIONAL MEMBERS (creature-specific): + - string weapon_type + - string behavior_state + - int wander_direction + - etc. +*/ + +// Example: +/* +class Goblin { + int position; + int health; + int sound_handle; // REQUIRED: Must track for death cleanup + string voice_sound; + timer move_timer; + timer voice_timer; // Renamed from alert_timer/groan_timer for clarity + timer attack_timer; + int next_voice_delay; + + // Creature-specific behavior + string weapon_type; // Example: "dagger" or "club" + + Goblin(int pos) { + position = pos; + health = GOBLIN_HEALTH; + sound_handle = -1; // CRITICAL: Always initialize to -1 + + // Choose random voice sound + int sound_index = random(0, goblin_sounds.length() - 1); + voice_sound = goblin_sounds[sound_index]; + + // Choose random weapon + weapon_type = (random(0, 1) == 0) ? "dagger" : "club"; + + // Initialize timers + move_timer.restart(); + voice_timer.restart(); + attack_timer.restart(); + next_voice_delay = random(GOBLIN_ALERT_MIN_DELAY, GOBLIN_ALERT_MAX_DELAY); + } +} +Goblin@[] goblins; // Global array to store all active goblins +*/ + +/* ============================================================================ + STEP 4: Spawn function + ============================================================================ + + REQUIRED AUDIO: Use play_creature_voice() and store the handle +*/ + +// Example: +/* +void spawn_goblin() { + // Find spawn location (avoid duplicates) + int spawn_x = -1; + for (int attempts = 0; attempts < 20; attempts++) { + int candidate = random(BASE_END + 1, MAP_SIZE - 1); + if (get_goblin_at(candidate) == null) { + spawn_x = candidate; + break; + } + } + if (spawn_x == -1) { + spawn_x = random(BASE_END + 1, MAP_SIZE - 1); + } + + Goblin@ g = Goblin(spawn_x); + goblins.insert_last(g); + + // REQUIRED: Use creature_audio system and store handle + g.sound_handle = play_creature_voice(g.voice_sound, x, spawn_x, GOBLIN_SOUND_VOLUME_STEP); +} +*/ + +/* ============================================================================ + STEP 5: Update function + ============================================================================ + + REQUIRED AUDIO: + - Periodic voice: play_creature_voice() and store handle + - Movement: play_creature_footstep() + - Attacks: play_creature_attack_sound() +*/ + +// Example: +/* +void update_goblin(Goblin@ goblin) { + // Play periodic voice sound + if (goblin.voice_timer.elapsed > goblin.next_voice_delay) { + goblin.voice_timer.restart(); + goblin.next_voice_delay = random(GOBLIN_ALERT_MIN_DELAY, GOBLIN_ALERT_MAX_DELAY); + // REQUIRED: Store handle for cleanup on death + goblin.sound_handle = play_creature_voice(goblin.voice_sound, x, goblin.position, GOBLIN_SOUND_VOLUME_STEP); + } + + // Try to attack player + if (try_attack_player_goblin(goblin)) { + return; + } + + // Movement logic + if (goblin.move_timer.elapsed < GOBLIN_MOVE_INTERVAL) return; + goblin.move_timer.restart(); + + // [Your movement AI here - pathfinding toward player, wandering, etc.] + + // When goblin moves: + goblin.position = target_x; + // REQUIRED: Use creature_audio for footsteps + play_creature_footstep(x, goblin.position, BASE_END, GRASS_END, GOBLIN_FOOTSTEP_MAX_DISTANCE, GOBLIN_SOUND_VOLUME_STEP); +} +*/ + +/* ============================================================================ + STEP 6: Attack functions + ============================================================================ + + REQUIRED AUDIO: Use play_creature_attack_sound() for weapon sounds +*/ + +// Example: +/* +bool try_attack_player_goblin(Goblin@ goblin) { + if (player_health <= 0) return false; + if (abs(goblin.position - x) > 1) return false; + if (goblin.attack_timer.elapsed < GOBLIN_ATTACK_INTERVAL) return false; + + goblin.attack_timer.restart(); + + // REQUIRED: Positional weapon sounds using creature_audio + if (goblin.weapon_type == "dagger") { + play_creature_attack_sound("sounds/weapons/dagger_swing.ogg", x, goblin.position, GOBLIN_SOUND_VOLUME_STEP); + } else { + play_creature_attack_sound("sounds/weapons/club_swing.ogg", x, goblin.position, GOBLIN_SOUND_VOLUME_STEP); + } + + int damage = random(GOBLIN_DAMAGE_MIN, GOBLIN_DAMAGE_MAX); + player_health -= damage; + if (player_health < 0) player_health = 0; + + // REQUIRED: Hit sound using creature_audio + play_creature_attack_sound("sounds/enemies/player_hit.ogg", x, goblin.position, GOBLIN_SOUND_VOLUME_STEP); + + return true; +} +*/ + +/* ============================================================================ + STEP 7: Damage/Death function + ============================================================================ + + CRITICAL: MUST stop creature's sound before playing death sound +*/ + +// Example: +/* +bool damage_goblin_at(int pos, int damage) { + for (uint i = 0; i < goblins.length(); i++) { + if (goblins[i].position == pos) { + goblins[i].health -= damage; + if (goblins[i].health <= 0) { + // CRITICAL: Stop active sounds before death sound + if (goblins[i].sound_handle != -1) { + p.destroy_sound(goblins[i].sound_handle); + goblins[i].sound_handle = -1; + } + // REQUIRED: Use creature_audio for death sound + play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, GOBLIN_SOUND_VOLUME_STEP); + goblins.remove_at(i); + } + return true; + } + } + return false; +} +*/ + +/* ============================================================================ + STEP 8: Helper functions + ============================================================================ */ + +// Example: +/* +Goblin@ get_goblin_at(int pos) { + for (uint i = 0; i < goblins.length(); i++) { + if (goblins[i].position == pos) { + return @goblins[i]; + } + } + return null; +} + +void update_goblins() { + for (uint i = 0; i < goblins.length(); i++) { + update_goblin(goblins[i]); + } +} + +void clear_goblins() { + for (uint i = 0; i < goblins.length(); i++) { + if (goblins[i].sound_handle != -1) { + p.destroy_sound(goblins[i].sound_handle); + goblins[i].sound_handle = -1; + } + } + goblins.resize(0); +} +*/ + +/* ============================================================================ + STEP 9: Update src/combat.nvgt damage functions + ============================================================================ */ + +// Add to relevant damage functions in src/combat.nvgt: +/* +// In attack_enemy_ranged(): +if (damage_goblin_at(check_x, damage)) { + return check_x; +} + +// In attack_enemy(): +if (damage_goblin_at(target_x, damage)) { + return true; +} + +// When player damages goblin, play hit sound: +play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, GOBLIN_SOUND_VOLUME_STEP); +*/ + +/* ============================================================================ + CHECKLIST: Required for Every Creature + ============================================================================ + + Constants in constants.nvgt: + [ ] CREATURE_HEALTH + [ ] CREATURE_DAMAGE_MIN / CREATURE_DAMAGE_MAX + [ ] CREATURE_MOVE_INTERVAL + [ ] CREATURE_ATTACK_INTERVAL + [ ] CREATURE_ALERT_MIN_DELAY / CREATURE_ALERT_MAX_DELAY + [ ] CREATURE_FOOTSTEP_MAX_DISTANCE + [ ] CREATURE_SOUND_VOLUME_STEP + + Class members: + [ ] int position + [ ] int health + [ ] int sound_handle (initialized to -1) + [ ] timer move_timer + [ ] timer attack_timer + [ ] timer voice_timer + [ ] int next_voice_delay + [ ] string voice_sound + + Audio usage: + [ ] Spawn: play_creature_voice() with stored handle + [ ] Update: play_creature_voice() with stored handle + [ ] Movement: play_creature_footstep() + [ ] Attacks: play_creature_attack_sound() for weapons + [ ] Death: Stop sound_handle BEFORE play_creature_death_sound() + [ ] Player damages creature: play_creature_hit_sound() + + Functions: + [ ] spawn_creature() + [ ] update_creature() + [ ] try_attack_player_creature() + [ ] damage_creature_at() + [ ] get_creature_at() + [ ] update_creatures() + [ ] clear_creatures() + + Integration: + [ ] Add creature to combat.nvgt damage checks + [ ] Call update_creatures() in main game loop + [ ] Handle cleanup (clear_creatures on game over, etc.) + + ============================================================================ */ diff --git a/src/environment.nvgt b/src/environment.nvgt index 759213c..978d1d2 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -1,3 +1,34 @@ +// Centralized falling damage system +// Safe fall height is 10 feet or less +// Each foot above 10 has a chance to deal 0-4 damage +// This means falling from great heights is VERY dangerous but not guaranteed fatal +const int SAFE_FALL_HEIGHT = 10; +const int FALL_DAMAGE_MIN = 0; +const int FALL_DAMAGE_MAX = 4; + +void apply_falling_damage(int fall_height) { + // Always play the hit ground sound + p.play_stationary("sounds/actions/hit_ground.ogg", false); + + if (fall_height <= SAFE_FALL_HEIGHT) { + screen_reader_speak("Landed safely.", true); + return; + } + + // Calculate damage: roll 0-4 for each foot above 10 + int damage = 0; + for (int i = SAFE_FALL_HEIGHT; i < fall_height; i++) { + damage += random(FALL_DAMAGE_MIN, FALL_DAMAGE_MAX); + } + + // Apply damage + player_health -= damage; + if (player_health < 0) player_health = 0; + + // Feedback + screen_reader_speak("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true); +} + // Tree Object class Tree { int position; @@ -45,7 +76,7 @@ class Tree { int tree_distance = x - position; if (tree_distance < 0) tree_distance = -tree_distance; - if (tree_distance <= 4) { + if (tree_distance <= TREE_SOUND_RANGE) { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true); if (sound_handle != -1) { @@ -140,10 +171,16 @@ class Tree { Tree@[] trees; bool tree_too_close(int pos) { + // Check distance from base (must be at least 5 tiles away) + if (pos <= BASE_END + 5) { + return true; + } + + // Check distance from other trees (must be at least 10 tiles apart) for (uint i = 0; i < trees.length(); i++) { int distance = trees[i].position - pos; if (distance < 0) distance = -distance; - if (distance <= 5) { + if (distance < 10) { return true; } } @@ -235,6 +272,19 @@ void damage_tree(int target_x, int damage) { void perform_search(int current_x) { + // First priority: Check for world drops on this tile or adjacent + for (int check_x = current_x - 1; check_x <= current_x + 1; check_x++) { + WorldDrop@ drop = get_drop_at(check_x); + if (drop != null) { + if (!try_pickup_world_drop(drop)) { + return; + } + p.play_stationary("sounds/items/miscellaneous.ogg", false); + remove_drop_at(check_x); + return; + } + } + // Check for snares nearby (adjacent within range) for (int check_x = current_x - SNARE_COLLECT_RANGE; check_x <= current_x + SNARE_COLLECT_RANGE; check_x++) { // Skip current x? User said "beside". If on top, it breaks. @@ -543,23 +593,10 @@ void land_on_ground(int ground_level) { fall_sound_handle = -1; } - p.play_stationary("sounds/actions/hit_ground.ogg", false); - - // Calculate fall damage + // Calculate fall damage using centralized function (also plays hit_ground sound) int fall_height = fall_start_y - ground_level; y = ground_level; - - if (fall_height > 10) { - int damage = 0; - for (int i = 10; i < fall_height; i++) { - damage += random(1, 3); - } - player_health -= damage; - screen_reader_speak("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true); - } else { - screen_reader_speak("Landed safely.", true); - } - + apply_falling_damage(fall_height); fall_start_y = 0; } diff --git a/src/flying_creature_template.nvgt b/src/flying_creature_template.nvgt new file mode 100644 index 0000000..b49226b --- /dev/null +++ b/src/flying_creature_template.nvgt @@ -0,0 +1,71 @@ +// FLYING CREATURE TEMPLATE +// Use this as a guide when adding new flying small game (ducks, geese, etc.) +// +// This template ensures all flying creatures have consistent audio, spawning, and drops. + +/* ============================================================================ + STEP 1: Add creature sound(s) near the top of src/world_state.nvgt + ============================================================================ */ + +// Example: +// string[] duck_sounds = {"sounds/game/duck.ogg"}; + +/* ============================================================================ + STEP 2: Add constants to src/constants.nvgt + ============================================================================ */ + +// Example for "duck": +// const int DUCK_HEALTH = 1; +// const int DUCK_MOVE_INTERVAL_MIN = 900; +// const int DUCK_MOVE_INTERVAL_MAX = 2200; +// const int DUCK_FLYING_HEIGHT_MIN = 8; +// const int DUCK_FLYING_HEIGHT_MAX = 25; +// const float DUCK_SOUND_VOLUME_STEP = 3.0; +// const int DUCK_FLIGHT_SOUND_DELAY_MIN = 2500; +// const int DUCK_FLIGHT_SOUND_DELAY_MAX = 6000; +// const int DUCK_FALL_SPEED = 100; +// const int DUCK_FLY_AWAY_CHANCE = 2; +// const int DUCK_MAX_DIST_FROM_WATER = 4; +// const int DUCK_MAX_COUNT = 3; +// const int DUCK_HOURLY_SPAWN_CHANCE = 30; +// const int DUCK_SIGHT_RANGE = 6; + +/* ============================================================================ + STEP 3: Register config in init_flying_creature_configs() (src/world_state.nvgt) + ============================================================================ */ + +// Example: +// FlyingCreatureConfig@ duck_cfg = FlyingCreatureConfig(); +// duck_cfg.id = "duck"; +// duck_cfg.drop_type = "duck"; +// duck_cfg.sounds = duck_sounds; +// duck_cfg.fall_sound = "sounds/actions/falling.ogg"; +// duck_cfg.impact_sound = "sounds/game/game_falls.ogg"; +// duck_cfg.health = DUCK_HEALTH; +// duck_cfg.move_interval_min = DUCK_MOVE_INTERVAL_MIN; +// duck_cfg.move_interval_max = DUCK_MOVE_INTERVAL_MAX; +// duck_cfg.min_height = DUCK_FLYING_HEIGHT_MIN; +// duck_cfg.max_height = DUCK_FLYING_HEIGHT_MAX; +// duck_cfg.sound_volume_step = DUCK_SOUND_VOLUME_STEP; +// duck_cfg.sound_delay_min = DUCK_FLIGHT_SOUND_DELAY_MIN; +// duck_cfg.sound_delay_max = DUCK_FLIGHT_SOUND_DELAY_MAX; +// duck_cfg.fall_speed = DUCK_FALL_SPEED; +// duck_cfg.fly_away_chance = DUCK_FLY_AWAY_CHANCE; +// duck_cfg.max_dist_from_water = DUCK_MAX_DIST_FROM_WATER; +// duck_cfg.hourly_spawn_chance = DUCK_HOURLY_SPAWN_CHANCE; +// duck_cfg.max_count = DUCK_MAX_COUNT; +// duck_cfg.sight_range = DUCK_SIGHT_RANGE; +// duck_cfg.flee_on_sight = true; +// flying_creature_configs.insert_last(duck_cfg); + +/* ============================================================================ + STEP 4: Add butcher behavior (src/crafting.nvgt) if yields differ + ============================================================================ */ + +// Example: +// if (game_type == "duck") { +// inv_meat++; +// inv_feathers += random(2, 4); +// inv_down += random(1, 2); +// screen_reader_speak("Butchered duck. Got 1 meat, feathers, and down.", true); +// } diff --git a/src/inventory_items.nvgt b/src/inventory_items.nvgt index f451bab..aee93e1 100644 --- a/src/inventory_items.nvgt +++ b/src/inventory_items.nvgt @@ -10,6 +10,9 @@ string[] inv_small_game_types; // Array to track what types of small game we hav int inv_meat = 0; int inv_skins = 0; +int inv_feathers = 0; +int inv_down = 0; +int inv_incense = 0; int inv_spears = 0; int inv_snares = 0; @@ -37,6 +40,10 @@ int storage_small_game = 0; string[] storage_small_game_types; int storage_meat = 0; int storage_skins = 0; +int storage_feathers = 0; +int storage_down = 0; +int storage_incense = 0; + int storage_spears = 0; int storage_snares = 0; int storage_axes = 0; @@ -91,6 +98,9 @@ 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 ITEM_FEATHERS = 24; +const int ITEM_DOWN = 25; +const int ITEM_INCENSE = 26; const int HAT_MAX_HEALTH_BONUS = 1; const int GLOVES_MAX_HEALTH_BONUS = 1; const int PANTS_MAX_HEALTH_BONUS = 3; @@ -273,6 +283,9 @@ int get_personal_count(int item_type) { 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_FEATHERS) return inv_feathers; + if (item_type == ITEM_DOWN) return inv_down; + if (item_type == ITEM_INCENSE) return inv_incense; if (item_type == ITEM_SPEARS) return inv_spears; if (item_type == ITEM_SLINGS) return inv_slings; if (item_type == ITEM_AXES) return inv_axes; @@ -301,6 +314,9 @@ int get_storage_count(int item_type) { 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_FEATHERS) return storage_feathers; + if (item_type == ITEM_DOWN) return storage_down; + if (item_type == ITEM_INCENSE) return storage_incense; if (item_type == ITEM_SPEARS) return storage_spears; if (item_type == ITEM_SLINGS) return storage_slings; if (item_type == ITEM_AXES) return storage_axes; @@ -329,6 +345,9 @@ string get_item_label(int item_type) { 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_FEATHERS) return "feathers"; + if (item_type == ITEM_DOWN) return "down"; + if (item_type == ITEM_INCENSE) return "incense"; if (item_type == ITEM_SPEARS) return "spears"; if (item_type == ITEM_SLINGS) return "slings"; if (item_type == ITEM_AXES) return "axes"; @@ -366,6 +385,9 @@ string get_item_label_singular(int item_type) { 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_FEATHERS) return "feather"; + if (item_type == ITEM_DOWN) return "down"; + if (item_type == ITEM_INCENSE) return "incense stick"; if (item_type == ITEM_SPEARS) return "spear"; if (item_type == ITEM_SLINGS) return "sling"; if (item_type == ITEM_AXES) return "axe"; @@ -394,6 +416,9 @@ double get_item_favor_value(int item_type) { 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_FEATHERS) return 0.05; + if (item_type == ITEM_DOWN) return 0.05; + if (item_type == ITEM_INCENSE) return 0.10; if (item_type == ITEM_SPEARS) return 1.00; if (item_type == ITEM_SLINGS) return 2.00; if (item_type == ITEM_AXES) return 1.50; diff --git a/src/inventory_menus.nvgt b/src/inventory_menus.nvgt index 3dd1664..7e40d45 100644 --- a/src/inventory_menus.nvgt +++ b/src/inventory_menus.nvgt @@ -257,6 +257,9 @@ void deposit_item(int item_type) { 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_FEATHERS) { inv_feathers -= amount; storage_feathers += amount; } + else if (item_type == ITEM_DOWN) { inv_down -= amount; storage_down += amount; } + else if (item_type == ITEM_INCENSE) { inv_incense -= amount; storage_incense += 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; } @@ -301,6 +304,9 @@ void withdraw_item(int item_type) { 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_FEATHERS) { storage_feathers -= amount; inv_feathers += amount; } + else if (item_type == ITEM_DOWN) { storage_down -= amount; inv_down += amount; } + else if (item_type == ITEM_INCENSE) { storage_incense -= amount; inv_incense += 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; } @@ -341,6 +347,9 @@ void sacrifice_item(int item_type) { } else if (item_type == ITEM_MEAT) inv_meat--; else if (item_type == ITEM_SKINS) inv_skins--; + else if (item_type == ITEM_FEATHERS) inv_feathers--; + else if (item_type == ITEM_DOWN) inv_down--; + else if (item_type == ITEM_INCENSE) inv_incense--; else if (item_type == ITEM_SPEARS) inv_spears--; else if (item_type == ITEM_SLINGS) inv_slings--; else if (item_type == ITEM_AXES) inv_axes--; @@ -375,6 +384,9 @@ void build_personal_inventory_options(string[]@ options, int[]@ item_types) { 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("Feathers: " + inv_feathers); item_types.insert_last(ITEM_FEATHERS); + options.insert_last("Down: " + inv_down); item_types.insert_last(ITEM_DOWN); + options.insert_last("Incense: " + inv_incense); item_types.insert_last(ITEM_INCENSE); 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); @@ -404,6 +416,9 @@ void build_storage_inventory_options(string[]@ options, int[]@ item_types) { 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("Feathers: " + storage_feathers); item_types.insert_last(ITEM_FEATHERS); + options.insert_last("Down: " + storage_down); item_types.insert_last(ITEM_DOWN); + options.insert_last("Incense: " + storage_incense); item_types.insert_last(ITEM_INCENSE); 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); @@ -431,7 +446,10 @@ void show_inventory() { info += inv_clay + " clay, "; info += inv_small_game + " small game, "; info += inv_meat + " meat, "; - info += inv_skins + " skins. "; + info += inv_skins + " skins, "; + info += inv_feathers + " feathers, "; + info += inv_down + " down, "; + info += inv_incense + " incense. "; 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); @@ -550,12 +568,25 @@ void run_storage_menu() { } } -void run_altar_menu() { +void check_altar_menu(int player_x) { + if (!key_pressed(KEY_S)) return; + + // Must be in base + if (player_x > BASE_END) { + screen_reader_speak("Must be in base to use altar.", true); + return; + } + + // Must have altar if (world_altars.length() == 0) { screen_reader_speak("No altar built.", true); return; } + run_altar_menu(); +} + +void run_altar_menu() { screen_reader_speak("Altar. Favor " + format_favor(favor) + ".", true); int selection = 0; @@ -635,6 +666,26 @@ void try_feed_fire_log(WorldFire@ fire) { } } +void try_burn_incense() { + if (world_altars.length() == 0) { + screen_reader_speak("No altar built.", true); + return; + } + if (inv_clay_pots <= 0) { + screen_reader_speak("You need a clay pot to burn incense.", true); + return; + } + if (inv_incense <= 0) { + screen_reader_speak("No incense to burn.", true); + return; + } + + inv_incense--; + incense_hours_remaining += INCENSE_HOURS_PER_STICK; + incense_burning = true; + screen_reader_speak("Incense burning. " + incense_hours_remaining + " hours remaining.", true); +} + void check_equipment_menu() { if (key_pressed(KEY_E)) { // Check if player has any equipment @@ -678,6 +729,11 @@ void run_action_menu(int x) { } } + if (x <= BASE_END && world_altars.length() > 0 && inv_incense > 0) { + options.insert_last("Burn incense"); + action_types.insert_last(4); + } + while(true) { wait(5); menu_background_tick(); @@ -708,6 +764,8 @@ void run_action_menu(int x) { try_feed_fire_vine(nearby_fire); } else if (action == 3) { try_feed_fire_log(nearby_fire); + } else if (action == 4) { + try_burn_incense(); } break; } diff --git a/src/notify.nvgt b/src/notify.nvgt index 484aeff..3e88d6a 100644 --- a/src/notify.nvgt +++ b/src/notify.nvgt @@ -1,17 +1,18 @@ // Notification System string[] notification_history; const int MAX_NOTIFICATIONS = 10; +const int NOTIFICATION_DELAY = 3000; // 3 seconds between notifications int current_notification_index = -1; string[] notification_queue; -int[] notification_sound_handles; +timer notification_timer; +bool notification_active = false; +int notification_sound_handle = -1; void notify(string message) { - // Play notification sound - int sound_handle = p.play_stationary("sounds/notify.ogg", false); + // Add to queue (don't play yet) notification_queue.insert_last(message); - notification_sound_handles.insert_last(sound_handle); - // Add to history + // Add to history immediately so it appears in history even if queued notification_history.insert_last(message); // Keep only last 10 notifications @@ -25,17 +26,35 @@ void notify(string message) { void update_notifications() { if (notification_queue.length() == 0) { + notification_active = false; + notification_sound_handle = -1; return; } - int sound_handle = notification_sound_handles[0]; - if (sound_handle != -1 && p.sound_is_playing(sound_handle)) { + // If a notification is currently active, wait for delay + if (notification_active && notification_timer.elapsed < NOTIFICATION_DELAY) { return; } - screen_reader_speak(notification_queue[0], true); - notification_queue.remove_at(0); - notification_sound_handles.remove_at(0); + // If we're waiting for the notification sound to finish playing + if (notification_sound_handle != -1) { + // Check if sound is still playing + if (p.sound_is_active(notification_sound_handle)) { + return; // Still playing, wait + } + // Sound finished, now speak + screen_reader_speak(notification_queue[0], true); + notification_queue.remove_at(0); + notification_sound_handle = -1; + + // Start timer for next notification + notification_timer.restart(); + notification_active = true; + return; + } + + // Play next notification sound (don't speak yet) + notification_sound_handle = p.play_stationary("sounds/notify.ogg", false); } void check_notification_keys() { diff --git a/src/player.nvgt b/src/player.nvgt index 73310ce..80b1c41 100644 --- a/src/player.nvgt +++ b/src/player.nvgt @@ -36,6 +36,8 @@ int last_sling_stage = -1; // Track which stage we're in to avoid duplicate soun // Favor system double favor = 0.0; +int incense_hours_remaining = 0; +bool incense_burning = false; bool blessing_speed_active = false; timer blessing_speed_timer; diff --git a/src/save_system.nvgt b/src/save_system.nvgt index 55c4a99..e6d6022 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -145,7 +145,9 @@ void clear_world_objects() { clear_zombies(); clear_bandits(); + clear_flying_creatures(); clear_mountains(); + clear_world_drops(); } void reset_game_state() { @@ -174,6 +176,8 @@ void reset_game_state() { base_max_health = 10; max_health = 10; favor = 0.0; + incense_hours_remaining = 0; + incense_burning = false; blessing_speed_active = false; inv_stones = 0; @@ -186,6 +190,9 @@ void reset_game_state() { inv_small_game_types.resize(0); inv_meat = 0; inv_skins = 0; + inv_feathers = 0; + inv_down = 0; + inv_incense = 0; inv_spears = 0; inv_snares = 0; inv_axes = 0; @@ -211,6 +218,9 @@ void reset_game_state() { storage_small_game_types.resize(0); storage_meat = 0; storage_skins = 0; + storage_feathers = 0; + storage_down = 0; + storage_incense = 0; storage_spears = 0; storage_snares = 0; storage_axes = 0; @@ -283,6 +293,7 @@ void start_new_game() { spawn_trees(5, 19); init_barricade(); init_time(); + init_weather(); save_game_state(); } @@ -430,6 +441,8 @@ bool load_game_state_from_raw(const string&in rawData) { if (get_raw_number(rawData, "player_base_health", value)) base_max_health = value; if (get_raw_number(rawData, "player_max_health", value)) max_health = value; if (get_raw_number(rawData, "player_favor", value)) favor = value; + if (get_raw_number(rawData, "incense_hours_remaining", value)) incense_hours_remaining = value; + if (get_raw_bool(rawData, "incense_burning", bool_value)) incense_burning = bool_value; if (get_raw_number(rawData, "time_current_hour", value)) current_hour = value; 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; @@ -459,6 +472,9 @@ bool load_game_state_from_raw(const string&in rawData) { 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_feathers", value)) inv_feathers = value; + if (get_raw_number(rawData, "inventory_down", value)) inv_down = value; + if (get_raw_number(rawData, "inventory_incense", value)) inv_incense = 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; @@ -487,6 +503,7 @@ bool load_game_state_from_raw(const string&in rawData) { if (equipped_arms != EQUIP_POUCH) equipped_arms = EQUIP_NONE; if (equipped_arms == EQUIP_POUCH && inv_skin_pouches <= 0) equipped_arms = EQUIP_NONE; + if (incense_hours_remaining > 0) incense_burning = true; if (inv_small_game_types.length() == 0 && inv_small_game > 0) { for (int i = 0; i < inv_small_game; i++) { @@ -518,6 +535,8 @@ bool save_game_state() { saveData.set("player_base_health", base_max_health); saveData.set("player_max_health", max_health); saveData.set("player_favor", favor); + saveData.set("incense_hours_remaining", incense_hours_remaining); + saveData.set("incense_burning", incense_burning); saveData.set("inventory_stones", inv_stones); saveData.set("inventory_sticks", inv_sticks); @@ -528,6 +547,9 @@ bool save_game_state() { saveData.set("inventory_small_game", inv_small_game); saveData.set("inventory_meat", inv_meat); saveData.set("inventory_skins", inv_skins); + saveData.set("inventory_feathers", inv_feathers); + saveData.set("inventory_down", inv_down); + saveData.set("inventory_incense", inv_incense); saveData.set("inventory_spears", inv_spears); saveData.set("inventory_snares", inv_snares); saveData.set("inventory_axes", inv_axes); @@ -554,6 +576,9 @@ bool save_game_state() { saveData.set("storage_small_game", storage_small_game); saveData.set("storage_meat", storage_meat); saveData.set("storage_skins", storage_skins); + saveData.set("storage_feathers", storage_feathers); + saveData.set("storage_down", storage_down); + saveData.set("storage_incense", storage_incense); saveData.set("storage_spears", storage_spears); saveData.set("storage_snares", storage_snares); saveData.set("storage_axes", storage_axes); @@ -605,6 +630,8 @@ bool save_game_state() { } saveData.set("quest_queue", join_string_array(questData)); + saveData.set("weather_data", serialize_weather()); + 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); @@ -685,6 +712,12 @@ bool save_game_state() { } saveData.set("mountains_data", join_string_array(mountainData)); + string[] dropData; + for (uint i = 0; i < world_drops.length(); i++) { + dropData.insert_last(world_drops[i].position + "|" + world_drops[i].type); + } + saveData.set("drops_data", join_string_array(dropData)); + string rawData = saveData.serialize(); string encryptedData = encrypt_save_data(rawData); return save_data(SAVE_FILE_PATH, encryptedData); @@ -755,6 +788,9 @@ bool load_game_state() { max_health = int(get_number(saveData, "player_max_health", 10)); base_max_health = int(get_number(saveData, "player_base_health", max_health)); favor = get_number(saveData, "player_favor", 0.0); + incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0)); + incense_burning = get_bool(saveData, "incense_burning", false); + if (incense_hours_remaining > 0) incense_burning = true; if (x < 0) x = 0; if (x >= MAP_SIZE) x = MAP_SIZE - 1; @@ -770,6 +806,9 @@ bool load_game_state() { inv_small_game = int(get_number(saveData, "inventory_small_game", 0)); inv_meat = int(get_number(saveData, "inventory_meat", 0)); inv_skins = int(get_number(saveData, "inventory_skins", 0)); + inv_feathers = int(get_number(saveData, "inventory_feathers", 0)); + inv_down = int(get_number(saveData, "inventory_down", 0)); + inv_incense = int(get_number(saveData, "inventory_incense", 0)); inv_spears = int(get_number(saveData, "inventory_spears", 0)); inv_snares = int(get_number(saveData, "inventory_snares", 0)); inv_axes = int(get_number(saveData, "inventory_axes", 0)); @@ -809,6 +848,9 @@ bool load_game_state() { 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_feathers = int(get_number(saveData, "storage_feathers", 0)); + storage_down = int(get_number(saveData, "storage_down", 0)); + storage_incense = int(get_number(saveData, "storage_incense", 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)); @@ -900,6 +942,13 @@ bool load_game_state() { is_daytime = (current_hour >= 6 && current_hour < 19); hour_timer.restart(); + string weather_data; + if (saveData.get("weather_data", weather_data) && weather_data.length() > 0) { + deserialize_weather(weather_data); + } else { + init_weather(); + } + string[] treeData = get_string_list_or_split(saveData, "trees_data"); for (uint i = 0; i < treeData.length(); i++) { string[]@ parts = treeData[i].split("|"); @@ -1061,6 +1110,13 @@ bool load_game_state() { world_mountains.insert_last(mountain); } + string[] dropData = get_string_list_or_split(saveData, "drops_data"); + for (uint i = 0; i < dropData.length(); i++) { + string[]@ parts = dropData[i].split("|"); + if (parts.length() < 2) continue; + add_world_drop(parse_int(parts[0]), parts[1]); + } + update_ambience(true); return true; } diff --git a/src/time_system.nvgt b/src/time_system.nvgt index 2365e1b..1d5e3a2 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -104,9 +104,15 @@ void expand_regular_area() { notify("A " + width_desc + " stream flows through the new area at x " + actual_start + "."); } else { - int tree_pos = random(new_start, new_end); - Tree@ t = Tree(tree_pos); - trees.insert_last(t); + // Try to place a tree with proper spacing + for (int attempt = 0; attempt < 20; attempt++) { + int tree_pos = random(new_start, new_end); + if (!tree_too_close(tree_pos)) { + Tree@ t = Tree(tree_pos); + trees.insert_last(t); + break; + } + } } area_expanded_today = true; @@ -210,16 +216,49 @@ void attempt_resident_recruitment() { return; } - int chance = random(25, 35); + // Check if base is full + if (residents_count >= MAX_RESIDENTS) { + return; + } + + // Recruitment chance based on storage buildings + int storage_count = world_storages.length(); + int min_chance = 50; + int max_chance = 60; + + if (storage_count >= 2) { + // 2+ storage: 75-100% chance + min_chance = 75; + max_chance = 100; + } else if (storage_count == 1) { + // 1 storage: 50-75% chance + min_chance = 50; + max_chance = 75; + } + // 0 storage: 50-60% chance (defaults above) + + int chance = random(min_chance, max_chance); int roll = random(1, 100); if (roll > chance) { return; } int added = random(1, 3); + // Don't exceed cap + if (residents_count + added > MAX_RESIDENTS) { + added = MAX_RESIDENTS - residents_count; + } + + if (added <= 0) return; + residents_count += added; string join_message = (added == 1) ? "A survivor joins your base." : "" + added + " survivors join your base."; notify(join_message); + + // Notify if base is now full + if (residents_count >= MAX_RESIDENTS) { + notify("Your base is at maximum capacity."); + } } void end_invasion() { @@ -369,14 +408,34 @@ void update_time() { } attempt_daily_invasion(); keep_base_fires_fed(); + update_incense_burning(); + attempt_hourly_flying_creature_spawn(); check_scheduled_invasion(); attempt_blessing(); + check_weather_transition(); + attempt_resident_collection(); } + // Proactive resident defense with slings + attempt_resident_sling_defense(); + // Manage bandits during active invasion manage_bandits_during_invasion(); } +void update_incense_burning() { + if (!incense_burning || incense_hours_remaining <= 0) return; + + favor += INCENSE_FAVOR_PER_HOUR; + incense_hours_remaining--; + + if (incense_hours_remaining <= 0) { + incense_hours_remaining = 0; + incense_burning = false; + notify("The incense has burned out."); + } +} + void check_time_input() { if (key_pressed(KEY_T)) { screen_reader_speak(get_time_string(), true); diff --git a/src/ui.nvgt b/src/ui.nvgt index c9df2f8..0e7c6e6 100644 --- a/src/ui.nvgt +++ b/src/ui.nvgt @@ -6,7 +6,8 @@ string ui_input_box(const string title, const string prompt, const string defaul } int ui_question(const string title, const string prompt) { - int result = virtual_question(title, prompt); + // Put the prompt in both title (for screen reader) and message (for dialog to work) + int result = virtual_question(prompt, prompt); show_window("Draugnorak"); return result; } diff --git a/src/weather.nvgt b/src/weather.nvgt new file mode 100644 index 0000000..16586d3 --- /dev/null +++ b/src/weather.nvgt @@ -0,0 +1,492 @@ +// Weather System +// Provides ambient wind, rain, and thunder effects + +// Weather states +const int WEATHER_CLEAR = 0; +const int WEATHER_WINDY = 1; +const int WEATHER_RAINY = 2; +const int WEATHER_STORMY = 3; + +// Intensity levels (0 = none, 1-3 = low/medium/high) +const int INTENSITY_NONE = 0; +const int INTENSITY_LOW = 1; +const int INTENSITY_MEDIUM = 2; +const int INTENSITY_HIGH = 3; + +// Audio fade settings +const int WEATHER_FADE_DURATION = 8000; // 8 seconds for smooth transitions +const float WEATHER_MIN_VOLUME = -30.0; +const float WEATHER_MAX_VOLUME = 0.0; + +// Rain volume levels by intensity +const float RAIN_VOLUME_LIGHT = -18.0; +const float RAIN_VOLUME_MODERATE = -10.0; +const float RAIN_VOLUME_HEAVY = -3.0; + +// Wind gust settings +const int WIND_GUST_MIN_DELAY = 10000; // Min 10 seconds between gusts +const int WIND_GUST_MAX_DELAY = 25000; // Max 25 seconds between gusts + +// Thunder timing +const int THUNDER_MIN_INTERVAL = 8000; // Min 8 seconds between thunder +const int THUNDER_MAX_INTERVAL = 35000; // Max 35 seconds between thunder +const int THUNDER_MOVEMENT_SPEED = 2000; // ms per tile movement (slow roll across sky) +const float THUNDER_SOUND_VOLUME_STEP = 2.0; // Gentler volume falloff +const int THUNDER_SPAWN_DISTANCE_MIN = 20; // Min distance from player +const int THUNDER_SPAWN_DISTANCE_MAX = 40; // Max distance from player + +// Weather transition chances (out of 100) +const int CHANCE_CLEAR_TO_WINDY = 15; +const int CHANCE_CLEAR_TO_RAINY = 4; +const int CHANCE_CLEAR_TO_STORMY = 2; +const int CHANCE_WINDY_STAY = 55; +const int CHANCE_WINDY_TO_CLEAR = 25; +const int CHANCE_WINDY_TO_STORMY = 12; +const int CHANCE_RAINY_STAY = 45; +const int CHANCE_RAINY_TO_STORMY = 25; +const int CHANCE_STORMY_STAY = 40; +const int CHANCE_STORMY_TO_RAINY = 35; + +// State variables +int weather_state = WEATHER_CLEAR; +int wind_intensity = INTENSITY_NONE; +int rain_intensity = INTENSITY_NONE; +bool thunder_enabled = false; + +// Wind gust state +timer wind_gust_timer; +int next_wind_gust_delay = 0; +int wind_sound_handle = -1; + +// Rain state +int rain_sound_handle = -1; + +// Fade state for rain +bool rain_fading = false; +float rain_fade_from_volume = WEATHER_MIN_VOLUME; +float rain_fade_to_volume = WEATHER_MIN_VOLUME; +timer rain_fade_timer; + +// Thunder object state +class ThunderStrike { + int position; + int direction; // -1 = moving west, 1 = moving east + int sound_handle; + timer movement_timer; + + ThunderStrike(int pos, int dir, int handle) { + position = pos; + direction = dir; + sound_handle = handle; + movement_timer.restart(); + } + + void update() { + if (movement_timer.elapsed >= THUNDER_MOVEMENT_SPEED) { + position += direction; + movement_timer.restart(); + } + + // Update sound position + if (sound_handle != -1 && p.sound_is_active(sound_handle)) { + p.update_sound_1d(sound_handle, position); + } + } + + bool is_finished() { + return sound_handle == -1 || !p.sound_is_active(sound_handle); + } +} + +ThunderStrike@[] active_thunder; +timer thunder_timer; +int next_thunder_interval = 0; + +// Sound file paths +string[] wind_sounds = { + "", // INTENSITY_NONE placeholder + "sounds/nature/wind_low.ogg", + "sounds/nature/wind_medium.ogg", + "sounds/nature/wind_high.ogg" +}; + +string[] thunder_sounds = { + "sounds/nature/thunder_low.ogg", + "sounds/nature/thunder_medium.ogg", + "sounds/nature/thunder_high.ogg" +}; + +const string RAIN_SOUND = "sounds/nature/rain.ogg"; + +void init_weather() { + weather_state = WEATHER_CLEAR; + wind_intensity = INTENSITY_NONE; + rain_intensity = INTENSITY_NONE; + thunder_enabled = false; + wind_sound_handle = -1; + rain_sound_handle = -1; + rain_fading = false; + wind_gust_timer.restart(); + next_wind_gust_delay = 0; + active_thunder.resize(0); + thunder_timer.restart(); + next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL); +} + +void update_weather() { + update_wind_gusts(); + update_rain_fade(); + update_thunder(); +} + +// Called each game hour from time_system +void check_weather_transition() { + int roll = random(1, 100); + + if (weather_state == WEATHER_CLEAR) { + if (roll <= CHANCE_CLEAR_TO_STORMY) { + start_storm(); + } else if (roll <= CHANCE_CLEAR_TO_STORMY + CHANCE_CLEAR_TO_RAINY) { + start_rain(); + } else if (roll <= CHANCE_CLEAR_TO_STORMY + CHANCE_CLEAR_TO_RAINY + CHANCE_CLEAR_TO_WINDY) { + start_wind(); + } + } else if (weather_state == WEATHER_WINDY) { + if (roll <= CHANCE_WINDY_TO_CLEAR) { + clear_weather(); + } else if (roll <= CHANCE_WINDY_TO_CLEAR + CHANCE_WINDY_TO_STORMY) { + start_storm(); + } else if (roll <= CHANCE_WINDY_TO_CLEAR + CHANCE_WINDY_TO_STORMY + CHANCE_WINDY_STAY) { + // Stay windy, maybe change intensity + maybe_change_wind_intensity(); + } + } else if (weather_state == WEATHER_RAINY) { + if (roll <= CHANCE_RAINY_TO_STORMY) { + // Escalate to storm + weather_state = WEATHER_STORMY; + thunder_enabled = true; + thunder_timer.restart(); + next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL); + // Maybe increase rain + if (rain_intensity < INTENSITY_HIGH && random(1, 100) <= 50) { + fade_rain_to_intensity(rain_intensity + 1); + } + } else if (roll <= CHANCE_RAINY_TO_STORMY + CHANCE_RAINY_STAY) { + // Stay rainy, maybe change intensity + maybe_change_rain_intensity(); + } else { + // Clear up + clear_weather(); + } + } else if (weather_state == WEATHER_STORMY) { + if (roll <= CHANCE_STORMY_TO_RAINY) { + // De-escalate to just rain + weather_state = WEATHER_RAINY; + thunder_enabled = false; + // Clear any active thunder + for (uint i = 0; i < active_thunder.length(); i++) { + if (active_thunder[i].sound_handle != -1) { + p.destroy_sound(active_thunder[i].sound_handle); + } + } + active_thunder.resize(0); + // Maybe reduce rain + if (rain_intensity > INTENSITY_LOW && random(1, 100) <= 40) { + fade_rain_to_intensity(rain_intensity - 1); + } + // Stop wind if present + if (wind_intensity > INTENSITY_NONE) { + wind_intensity = INTENSITY_NONE; + if (wind_sound_handle != -1) { + p.destroy_sound(wind_sound_handle); + wind_sound_handle = -1; + } + } + } else if (roll <= CHANCE_STORMY_TO_RAINY + CHANCE_STORMY_STAY) { + // Stay stormy, maybe change intensities + maybe_change_storm_intensity(); + } else { + // Clear up + clear_weather(); + } + } +} + +void start_wind() { + weather_state = WEATHER_WINDY; + wind_intensity = random(INTENSITY_LOW, INTENSITY_MEDIUM); + wind_gust_timer.restart(); + next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY); +} + +void start_rain() { + weather_state = WEATHER_RAINY; + int new_intensity = random(INTENSITY_LOW, INTENSITY_MEDIUM); + fade_rain_to_intensity(new_intensity); +} + +void start_storm() { + weather_state = WEATHER_STORMY; + + // Start or increase wind + wind_intensity = random(INTENSITY_MEDIUM, INTENSITY_HIGH); + wind_gust_timer.restart(); + next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY); + + // Start rain + int new_rain = random(INTENSITY_MEDIUM, INTENSITY_HIGH); + fade_rain_to_intensity(new_rain); + + // Enable thunder + thunder_enabled = true; + thunder_timer.restart(); + next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL); +} + +void clear_weather() { + weather_state = WEATHER_CLEAR; + thunder_enabled = false; + + wind_intensity = INTENSITY_NONE; + if (wind_sound_handle != -1) { + p.destroy_sound(wind_sound_handle); + wind_sound_handle = -1; + } + + if (rain_intensity > INTENSITY_NONE) { + fade_rain_to_intensity(INTENSITY_NONE); + } + + // Clear any active thunder + for (uint i = 0; i < active_thunder.length(); i++) { + if (active_thunder[i].sound_handle != -1) { + p.destroy_sound(active_thunder[i].sound_handle); + } + } + active_thunder.resize(0); +} + +void maybe_change_wind_intensity() { + if (random(1, 100) <= 30) { + int change = random(-1, 1); + int new_intensity = wind_intensity + change; + if (new_intensity < INTENSITY_LOW) new_intensity = INTENSITY_LOW; + if (new_intensity > INTENSITY_HIGH) new_intensity = INTENSITY_HIGH; + wind_intensity = new_intensity; + } +} + +void maybe_change_rain_intensity() { + if (random(1, 100) <= 30) { + int change = random(-1, 1); + int new_intensity = rain_intensity + change; + if (new_intensity < INTENSITY_LOW) new_intensity = INTENSITY_LOW; + if (new_intensity > INTENSITY_HIGH) new_intensity = INTENSITY_HIGH; + if (new_intensity != rain_intensity) { + fade_rain_to_intensity(new_intensity); + } + } +} + +void maybe_change_storm_intensity() { + // Possibly change wind + if (random(1, 100) <= 25) { + int new_wind = wind_intensity + random(-1, 1); + if (new_wind < INTENSITY_LOW) new_wind = INTENSITY_LOW; + if (new_wind > INTENSITY_HIGH) new_wind = INTENSITY_HIGH; + wind_intensity = new_wind; + } + // Possibly change rain + if (random(1, 100) <= 25) { + int new_rain = rain_intensity + random(-1, 1); + if (new_rain < INTENSITY_LOW) new_rain = INTENSITY_LOW; + if (new_rain > INTENSITY_HIGH) new_rain = INTENSITY_HIGH; + if (new_rain != rain_intensity) { + fade_rain_to_intensity(new_rain); + } + } +} + +// Wind gust implementation +void update_wind_gusts() { + if (wind_intensity == INTENSITY_NONE) return; + + // Check if it's time for next gust + if (wind_gust_timer.elapsed >= next_wind_gust_delay) { + play_wind_gust(); + wind_gust_timer.restart(); + next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY); + } +} + +void play_wind_gust() { + if (wind_intensity == INTENSITY_NONE || wind_intensity > INTENSITY_HIGH) return; + + // Play the appropriate wind sound once (non-looping) + wind_sound_handle = p.play_stationary(wind_sounds[wind_intensity], false); +} + +// Rain fade implementation +float get_rain_target_volume(int intensity) { + if (intensity == INTENSITY_NONE) return WEATHER_MIN_VOLUME; + if (intensity == INTENSITY_LOW) return RAIN_VOLUME_LIGHT; + if (intensity == INTENSITY_MEDIUM) return RAIN_VOLUME_MODERATE; + return RAIN_VOLUME_HEAVY; +} + +void fade_rain_to_intensity(int new_intensity) { + if (new_intensity == rain_intensity && !rain_fading) return; + + // Complete any current fade + if (rain_fading) { + complete_rain_fade(); + } + + rain_fade_from_volume = get_rain_target_volume(rain_intensity); + rain_fade_to_volume = get_rain_target_volume(new_intensity); + + // Start rain sound if not playing + if (rain_sound_handle == -1 && new_intensity != INTENSITY_NONE) { + rain_sound_handle = p.play_stationary(RAIN_SOUND, true); + p.update_sound_start_values(rain_sound_handle, 0.0, WEATHER_MIN_VOLUME, 1.0); + rain_fade_from_volume = WEATHER_MIN_VOLUME; + } + + rain_fading = true; + rain_fade_timer.restart(); + rain_intensity = new_intensity; // Track target intensity +} + +void update_rain_fade() { + if (!rain_fading) return; + if (rain_sound_handle == -1) { + rain_fading = false; + return; + } + + float progress = float(rain_fade_timer.elapsed) / float(WEATHER_FADE_DURATION); + if (progress > 1.0) progress = 1.0; + + float current_vol = rain_fade_from_volume + ((rain_fade_to_volume - rain_fade_from_volume) * progress); + p.update_sound_start_values(rain_sound_handle, 0.0, current_vol, 1.0); + + if (progress >= 1.0) { + complete_rain_fade(); + } +} + +void complete_rain_fade() { + rain_fading = false; + + // If faded to silence, destroy sound + if (rain_fade_to_volume <= WEATHER_MIN_VOLUME && rain_sound_handle != -1) { + p.destroy_sound(rain_sound_handle); + rain_sound_handle = -1; + rain_intensity = INTENSITY_NONE; + } +} + +// Thunder implementation +void update_thunder() { + // Update existing thunder strikes + for (uint i = 0; i < active_thunder.length(); i++) { + active_thunder[i].update(); + } + + // Remove finished thunder strikes + for (uint i = 0; i < active_thunder.length(); i++) { + if (active_thunder[i].is_finished()) { + active_thunder.remove_at(i); + i--; + } + } + + if (!thunder_enabled) return; + + // Check if it's time for new thunder + if (thunder_timer.elapsed >= next_thunder_interval) { + spawn_thunder(); + thunder_timer.restart(); + next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL); + } +} + +void spawn_thunder() { + // Pick random thunder sound + int thunder_type = random(0, thunder_sounds.length() - 1); + string thunder_file = thunder_sounds[thunder_type]; + + // Spawn thunder at random distance from player + int distance = random(THUNDER_SPAWN_DISTANCE_MIN, THUNDER_SPAWN_DISTANCE_MAX); + // Randomly place to left or right of player + int direction = random(0, 1) == 0 ? -1 : 1; + int thunder_pos = x + (distance * direction); + + // Play sound at position with custom volume step + int handle = play_1d_with_volume_step(thunder_file, x, thunder_pos, false, THUNDER_SOUND_VOLUME_STEP); + + if (handle != -1) { + ThunderStrike@ strike = ThunderStrike(thunder_pos, direction, handle); + active_thunder.insert_last(strike); + } +} + +void stop_all_weather_sounds() { + if (wind_sound_handle != -1) { + p.destroy_sound(wind_sound_handle); + wind_sound_handle = -1; + } + if (rain_sound_handle != -1) { + p.destroy_sound(rain_sound_handle); + rain_sound_handle = -1; + } + for (uint i = 0; i < active_thunder.length(); i++) { + if (active_thunder[i].sound_handle != -1) { + p.destroy_sound(active_thunder[i].sound_handle); + } + } + active_thunder.resize(0); + rain_fading = false; + wind_intensity = INTENSITY_NONE; + rain_intensity = INTENSITY_NONE; + thunder_enabled = false; +} + +// Save/Load functions +string serialize_weather() { + return weather_state + "|" + wind_intensity + "|" + rain_intensity + "|" + (thunder_enabled ? 1 : 0); +} + +void deserialize_weather(string data) { + string[]@ parts = data.split("|"); + if (parts.length() < 4) { + init_weather(); + return; + } + + // Stop any current sounds first + stop_all_weather_sounds(); + + weather_state = parse_int(parts[0]); + wind_intensity = parse_int(parts[1]); + int saved_rain = parse_int(parts[2]); + thunder_enabled = parse_int(parts[3]) == 1; + + // Restore wind gust timer + if (wind_intensity > INTENSITY_NONE && wind_intensity <= INTENSITY_HIGH) { + wind_gust_timer.restart(); + next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY); + } + + // Restore rain at saved intensity (no fade, instant) + if (saved_rain > INTENSITY_NONE && saved_rain <= INTENSITY_HIGH) { + rain_sound_handle = p.play_stationary(RAIN_SOUND, true); + float vol = get_rain_target_volume(saved_rain); + p.update_sound_start_values(rain_sound_handle, 0.0, vol, 1.0); + rain_intensity = saved_rain; + } + + // Reset thunder timer + thunder_timer.restart(); + next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL); +} diff --git a/src/world_state.nvgt b/src/world_state.nvgt index f4316aa..02ad9d3 100644 --- a/src/world_state.nvgt +++ b/src/world_state.nvgt @@ -9,6 +9,7 @@ int residents_count = 0; string[] zombie_sounds = {"sounds/enemies/zombie1.ogg"}; string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"}; +string[] goose_sounds = {"sounds/game/goose.ogg"}; class Zombie { int position; @@ -82,11 +83,174 @@ class Bandit { } Bandit@[] bandits; +class FlyingCreatureConfig { + string id; + string drop_type; + string[] sounds; + string fall_sound; + string impact_sound; + int health; + int move_interval_min; + int move_interval_max; + int min_height; + int max_height; + float sound_volume_step; + int sound_delay_min; + int sound_delay_max; + int fall_speed; + int fly_away_chance; + int max_dist_from_water; + int hourly_spawn_chance; + int max_count; + int sight_range; + bool flee_on_sight; +} +FlyingCreatureConfig@[] flying_creature_configs; + +class FlyingCreature { + int position; + int health; + int height; + string state; // "flying", "falling" + int area_start; + int area_end; + string creature_type; + int sound_handle; + int fall_sound_handle; + timer move_timer; + timer sound_timer; + timer fall_timer; + int next_move_delay; + int next_sound_delay; + string voice_sound; + bool fading_out; + bool ready_to_remove; + timer fade_timer; + + FlyingCreature(string type, int pos, int home_start, int home_end, FlyingCreatureConfig@ cfg) { + position = pos; + health = cfg.health; + height = random(cfg.min_height, cfg.max_height); + state = "flying"; + area_start = home_start; + area_end = home_end; + creature_type = type; + sound_handle = -1; + fall_sound_handle = -1; + + if (cfg.sounds.length() > 0) { + voice_sound = cfg.sounds[random(0, cfg.sounds.length() - 1)]; + } + + move_timer.restart(); + sound_timer.restart(); + + next_move_delay = random(cfg.move_interval_min, cfg.move_interval_max); + next_sound_delay = random(cfg.sound_delay_min, cfg.sound_delay_max); + fading_out = false; + ready_to_remove = false; + } +} +FlyingCreature@[] flying_creatures; + string get_random_small_game() { int index = random(0, small_game_types.length() - 1); return small_game_types[index]; } +class WorldDrop { + int position; + string type; + int sound_handle; + + WorldDrop(int pos, string t) { + position = pos; + type = t; + sound_handle = -1; + // Start looping item sound at position + sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true); + if (sound_handle != -1) { + p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true); + } + } + + void update() { + if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { + sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true); + if (sound_handle != -1) { + p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true); + } + } else { + // Update source position for 1d sound + p.update_sound_1d(sound_handle, position); + } + } + + void destroy() { + if (sound_handle != -1) { + p.destroy_sound(sound_handle); + sound_handle = -1; + } + } +} +WorldDrop@[] world_drops; + +void add_world_drop(int pos, string type) { + WorldDrop@ d = WorldDrop(pos, type); + world_drops.insert_last(d); +} + +void update_world_drops() { + for (uint i = 0; i < world_drops.length(); i++) { + world_drops[i].update(); + } +} + +WorldDrop@ get_drop_at(int pos) { + for (uint i = 0; i < world_drops.length(); i++) { + if (world_drops[i].position == pos) { + return @world_drops[i]; + } + } + return null; +} + +void remove_drop_at(int pos) { + for (uint i = 0; i < world_drops.length(); i++) { + if (world_drops[i].position == pos) { + world_drops[i].destroy(); + world_drops.remove_at(i); + return; + } + } +} + +void clear_world_drops() { + for (uint i = 0; i < world_drops.length(); i++) { + world_drops[i].destroy(); + } + world_drops.resize(0); +} + +bool try_pickup_small_game(string game_type) { + if (inv_small_game >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more small game.", true); + return false; + } + inv_small_game++; + inv_small_game_types.insert_last(game_type); + screen_reader_speak("Picked up " + game_type + ".", true); + return true; +} + +bool try_pickup_world_drop(WorldDrop@ drop) { + if (get_flying_creature_config_by_drop_type(drop.type) !is null) { + return try_pickup_small_game(drop.type); + } + screen_reader_speak("Picked up " + drop.type + ".", true); + return true; +} + class WorldSnare { int position; bool has_catch; @@ -843,9 +1007,9 @@ bool try_attack_player_bandit(Bandit@ bandit) { // Play weapon swing sound based on bandit's weapon if (bandit.weapon_type == "spear") { - p.play_stationary("sounds/weapons/spear_swing.ogg", false); + play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } else if (bandit.weapon_type == "axe") { - p.play_stationary("sounds/weapons/axe_swing.ogg", false); + play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX); @@ -856,9 +1020,9 @@ bool try_attack_player_bandit(Bandit@ bandit) { // Play hit sound if (bandit.weapon_type == "spear") { - p.play_stationary("sounds/weapons/spear_hit.ogg", false); + play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } else if (bandit.weapon_type == "axe") { - p.play_stationary("sounds/weapons/axe_hit.ogg", false); + play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } return true; @@ -877,11 +1041,11 @@ void try_attack_barricade_bandit(Bandit@ bandit) { // Play weapon swing sound if (bandit.weapon_type == "spear") { - p.play_stationary("sounds/weapons/spear_swing.ogg", false); - p.play_stationary("sounds/weapons/spear_hit.ogg", false); + play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); + play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } else if (bandit.weapon_type == "axe") { - p.play_stationary("sounds/weapons/axe_swing.ogg", false); - p.play_stationary("sounds/weapons/axe_hit.ogg", false); + play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); + play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } // Resident defense counter-attack @@ -1261,3 +1425,327 @@ void clear_mountains() { } world_mountains.resize(0); } + +// Flying Creature Functions + +void init_flying_creature_configs() { + flying_creature_configs.resize(0); + + FlyingCreatureConfig@ goose_cfg = FlyingCreatureConfig(); + goose_cfg.id = "goose"; + goose_cfg.drop_type = "goose"; + goose_cfg.sounds = goose_sounds; + goose_cfg.fall_sound = "sounds/actions/falling.ogg"; + goose_cfg.impact_sound = "sounds/game/game_falls.ogg"; + goose_cfg.health = GOOSE_HEALTH; + goose_cfg.move_interval_min = GOOSE_MOVE_INTERVAL_MIN; + goose_cfg.move_interval_max = GOOSE_MOVE_INTERVAL_MAX; + goose_cfg.min_height = GOOSE_FLYING_HEIGHT_MIN; + goose_cfg.max_height = GOOSE_FLYING_HEIGHT_MAX; + goose_cfg.sound_volume_step = GOOSE_SOUND_VOLUME_STEP; + goose_cfg.sound_delay_min = GOOSE_FLIGHT_SOUND_DELAY_MIN; + goose_cfg.sound_delay_max = GOOSE_FLIGHT_SOUND_DELAY_MAX; + goose_cfg.fall_speed = GOOSE_FALL_SPEED; + goose_cfg.fly_away_chance = GOOSE_FLY_AWAY_CHANCE; + goose_cfg.max_dist_from_water = GOOSE_MAX_DIST_FROM_WATER; + goose_cfg.hourly_spawn_chance = GOOSE_HOURLY_SPAWN_CHANCE; + goose_cfg.max_count = GOOSE_MAX_COUNT; + goose_cfg.sight_range = GOOSE_SIGHT_RANGE; + goose_cfg.flee_on_sight = false; + flying_creature_configs.insert_last(goose_cfg); +} + +FlyingCreatureConfig@ get_flying_creature_config(string creature_type) { + for (uint i = 0; i < flying_creature_configs.length(); i++) { + if (flying_creature_configs[i].id == creature_type) { + return @flying_creature_configs[i]; + } + } + return null; +} + +FlyingCreatureConfig@ get_flying_creature_config_by_drop_type(string drop_type) { + for (uint i = 0; i < flying_creature_configs.length(); i++) { + if (flying_creature_configs[i].drop_type == drop_type) { + return @flying_creature_configs[i]; + } + } + return null; +} + +void clear_flying_creatures() { + for (uint i = 0; i < flying_creatures.length(); i++) { + if (flying_creatures[i].sound_handle != -1) { + p.destroy_sound(flying_creatures[i].sound_handle); + flying_creatures[i].sound_handle = -1; + } + if (flying_creatures[i].fall_sound_handle != -1) { + p.destroy_sound(flying_creatures[i].fall_sound_handle); + flying_creatures[i].fall_sound_handle = -1; + } + } + flying_creatures.resize(0); +} + +FlyingCreature@ get_flying_creature_at(int pos) { + for (uint i = 0; i < flying_creatures.length(); i++) { + if (flying_creatures[i].position == pos) { + return @flying_creatures[i]; + } + } + return null; +} + +int get_flying_creature_count(string creature_type) { + int count = 0; + for (uint i = 0; i < flying_creatures.length(); i++) { + if (flying_creatures[i].creature_type == creature_type) { + count++; + } + } + return count; +} + +bool get_random_flying_creature_area(FlyingCreatureConfig@ cfg, int &out area_start, int &out area_end) { + int stream_count = int(world_streams.length()); + int mountain_stream_count = 0; + for (uint i = 0; i < world_mountains.length(); i++) { + mountain_stream_count += int(world_mountains[i].stream_positions.length()); + } + + int total_areas = stream_count + mountain_stream_count; + if (total_areas <= 0) return false; + + int pick = random(0, total_areas - 1); + if (pick < stream_count) { + area_start = world_streams[pick].start_position; + area_end = world_streams[pick].end_position; + } else { + pick -= stream_count; + for (uint i = 0; i < world_mountains.length(); i++) { + int local_count = int(world_mountains[i].stream_positions.length()); + if (pick < local_count) { + int stream_pos = world_mountains[i].start_position + world_mountains[i].stream_positions[pick]; + area_start = stream_pos; + area_end = stream_pos; + break; + } + pick -= local_count; + } + } + + area_start -= cfg.max_dist_from_water; + area_end += cfg.max_dist_from_water; + if (area_start < 0) area_start = 0; + if (area_end >= MAP_SIZE) area_end = MAP_SIZE - 1; + return true; +} + +bool find_flying_creature_spawn(FlyingCreatureConfig@ cfg, int &out spawn_x, int &out area_start, int &out area_end) { + if (!get_random_flying_creature_area(cfg, area_start, area_end)) return false; + + for (int attempts = 0; attempts < 20; attempts++) { + int candidate = random(area_start, area_end); + if (get_flying_creature_at(candidate) == null) { + spawn_x = candidate; + return true; + } + } + return false; +} + +void fly_away_flying_creature(FlyingCreature@ creature, FlyingCreatureConfig@ cfg) { + creature.state = "fading"; + creature.fading_out = true; + creature.ready_to_remove = false; + creature.fade_timer.restart(); + creature.health = 0; + + if (creature.sound_handle == -1 || !p.sound_is_active(creature.sound_handle)) { + creature.ready_to_remove = true; + } + if (creature.fall_sound_handle != -1) { + p.destroy_sound(creature.fall_sound_handle); + creature.fall_sound_handle = -1; + } +} + +bool spawn_flying_creature(string creature_type) { + FlyingCreatureConfig@ cfg = get_flying_creature_config(creature_type); + if (cfg is null) return false; + + int spawn_x = -1; + int area_start = 0; + int area_end = 0; + + if (!find_flying_creature_spawn(cfg, spawn_x, area_start, area_end)) { + return false; + } + + FlyingCreature@ c = FlyingCreature(creature_type, spawn_x, area_start, area_end, cfg); + flying_creatures.insert_last(c); + c.sound_handle = play_creature_voice(c.voice_sound, x, spawn_x, cfg.sound_volume_step); + return true; +} + +void update_flying_creature(FlyingCreature@ creature) { + FlyingCreatureConfig@ cfg = get_flying_creature_config(creature.creature_type); + if (cfg is null) return; + + if (creature.state == "fading") { + if (!creature.fading_out) { + creature.fading_out = true; + creature.fade_timer.restart(); + } + + if (creature.sound_handle != -1 && p.sound_is_active(creature.sound_handle)) { + float progress = float(creature.fade_timer.elapsed) / float(FLYING_CREATURE_FADE_OUT_DURATION); + if (progress < 0.0) progress = 0.0; + if (progress > 1.0) progress = 1.0; + float volume = 0.0 + (FLYING_CREATURE_FADE_OUT_MIN_VOLUME * progress); + p.update_sound_start_values(creature.sound_handle, 0.0, volume, 1.0); + } + + if (creature.fade_timer.elapsed >= FLYING_CREATURE_FADE_OUT_DURATION) { + if (creature.sound_handle != -1) { + p.destroy_sound(creature.sound_handle); + creature.sound_handle = -1; + } + creature.ready_to_remove = true; + } + return; + } + + if (creature.state == "flying") { + if (creature.position < creature.area_start || creature.position > creature.area_end) { + fly_away_flying_creature(creature, cfg); + return; + } + + if (creature.sound_timer.elapsed > creature.next_sound_delay) { + creature.sound_timer.restart(); + creature.next_sound_delay = random(cfg.sound_delay_min, cfg.sound_delay_max); + creature.sound_handle = play_creature_voice(creature.voice_sound, x, creature.position, cfg.sound_volume_step); + } + + if (cfg.fly_away_chance > 0 && random(1, 1000) <= cfg.fly_away_chance) { + fly_away_flying_creature(creature, cfg); + return; + } + + if (creature.move_timer.elapsed > creature.next_move_delay) { + creature.move_timer.restart(); + creature.next_move_delay = random(cfg.move_interval_min, cfg.move_interval_max); + + int dir = 0; + if (cfg.flee_on_sight && cfg.sight_range > 0) { + int distance_to_player = abs(x - creature.position); + if (distance_to_player <= cfg.sight_range) { + if (x > creature.position) dir = -1; + else if (x < creature.position) dir = 1; + } + } + if (dir == 0) dir = random(-1, 1); + if (dir != 0) { + int target_x = creature.position + dir; + if (target_x < creature.area_start || target_x > creature.area_end) { + fly_away_flying_creature(creature, cfg); + return; + } + if (target_x >= 0 && target_x < MAP_SIZE) { + creature.position = target_x; + if (creature.sound_handle != -1 && p.sound_is_active(creature.sound_handle)) { + p.update_sound_1d(creature.sound_handle, creature.position); + } + } + } + } + } else if (creature.state == "falling") { + if (creature.fall_timer.elapsed > cfg.fall_speed) { + creature.fall_timer.restart(); + creature.height--; + + if (creature.fall_sound_handle != -1) { + p.destroy_sound(creature.fall_sound_handle); + } + + float pitch_percent = 50.0 + (50.0 * (float(creature.height) / float(cfg.max_height))); + if (pitch_percent < 50.0) pitch_percent = 50.0; + if (pitch_percent > 100.0) pitch_percent = 100.0; + + creature.fall_sound_handle = p.play_extended_1d(cfg.fall_sound, x, creature.position, 0, 0, true, 0, 0.0, 0.0, pitch_percent); + if (creature.fall_sound_handle != -1) { + p.update_sound_positioning_values(creature.fall_sound_handle, -1.0, cfg.sound_volume_step, true); + } + + if (creature.height <= 0) { + if (creature.fall_sound_handle != -1) { + p.destroy_sound(creature.fall_sound_handle); + creature.fall_sound_handle = -1; + } + + play_creature_death_sound(cfg.impact_sound, x, creature.position, cfg.sound_volume_step); + notify("A " + creature.creature_type + " fell from the sky at " + creature.position + "!"); + add_world_drop(creature.position, cfg.drop_type); + creature.health = 0; + } + } + } +} + +void update_flying_creatures() { + for (uint i = 0; i < flying_creatures.length(); i++) { + update_flying_creature(flying_creatures[i]); + + if (flying_creatures[i].health <= 0) { + if (flying_creatures[i].state == "falling" && flying_creatures[i].height <= 0) { + flying_creatures.remove_at(i); + i--; + } else if (flying_creatures[i].state == "flying") { + flying_creatures.remove_at(i); + i--; + } else if (flying_creatures[i].state == "fading" && flying_creatures[i].ready_to_remove) { + flying_creatures.remove_at(i); + i--; + } + } + } +} + +void attempt_hourly_flying_creature_spawn() { + for (uint i = 0; i < flying_creature_configs.length(); i++) { + FlyingCreatureConfig@ cfg = flying_creature_configs[i]; + if (get_flying_creature_count(cfg.id) >= cfg.max_count) continue; + if (random(1, 100) <= cfg.hourly_spawn_chance) { + spawn_flying_creature(cfg.id); + } + } +} + +bool damage_flying_creature_at(int pos, int damage) { + for (uint i = 0; i < flying_creatures.length(); i++) { + if (flying_creatures[i].position == pos && flying_creatures[i].state == "flying") { + FlyingCreatureConfig@ cfg = get_flying_creature_config(flying_creatures[i].creature_type); + if (cfg is null) return false; + + flying_creatures[i].health -= damage; + if (flying_creatures[i].health <= 0) { + flying_creatures[i].state = "falling"; + flying_creatures[i].fall_timer.restart(); + + if (flying_creatures[i].sound_handle != -1) { + p.destroy_sound(flying_creatures[i].sound_handle); + flying_creatures[i].sound_handle = -1; + } + + float pitch_percent = 50.0 + (50.0 * (float(flying_creatures[i].height) / float(cfg.max_height))); + flying_creatures[i].fall_sound_handle = p.play_extended_1d(cfg.fall_sound, x, pos, 0, 0, true, 0, 0.0, 0.0, pitch_percent); + if (flying_creatures[i].fall_sound_handle != -1) { + p.update_sound_positioning_values(flying_creatures[i].fall_sound_handle, -1.0, cfg.sound_volume_step, true); + } + } + return true; + } + } + return false; +}