diff --git a/sounds/game/boar.ogg b/sounds/game/boar.ogg new file mode 100644 index 0000000..1b1a5eb --- /dev/null +++ b/sounds/game/boar.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93bd0347241c7f5de9b8886d5ddc51cdcd71fd4eba29ed71afac0dd3f71dd7a8 +size 69055 diff --git a/src/base_system.nvgt b/src/base_system.nvgt index bb64dfc..bc5c0ab 100644 --- a/src/base_system.nvgt +++ b/src/base_system.nvgt @@ -91,7 +91,6 @@ int perform_resident_defense() { // 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 @@ -213,7 +212,6 @@ void process_daily_weapon_breakage() { } // Resident resource collection -const int RESIDENT_COLLECTION_CHANCE = 10; // 10% chance per basket per hour void attempt_resident_collection() { // Only during daytime diff --git a/src/combat.nvgt b/src/combat.nvgt index e022a16..8317b8e 100644 --- a/src/combat.nvgt +++ b/src/combat.nvgt @@ -21,6 +21,10 @@ int attack_enemy_ranged(int start_x, int end_x, int damage) { if (damage_zombie_at(check_x, damage)) { return check_x; } + // Then check boars + if (damage_boar_at(check_x, damage)) { + return check_x; + } } return -1; } @@ -30,6 +34,10 @@ bool attack_enemy(int target_x, int damage) { if (damage_bandit_at(target_x, damage)) { return true; } + // Check boars + if (damage_boar_at(target_x, damage)) { + return true; + } // Then check zombies return damage_zombie_at(target_x, damage); } @@ -43,6 +51,8 @@ void perform_spear_attack(int current_x) { // Play hit sound based on enemy type (both use same hit sound for now) if (get_bandit_at(hit_pos) != null) { play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, hit_pos, BANDIT_SOUND_VOLUME_STEP); + } else if (get_boar_at(hit_pos) != null) { + play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, hit_pos, BOAR_SOUND_VOLUME_STEP); } else { play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, hit_pos, ZOMBIE_SOUND_VOLUME_STEP); } @@ -61,6 +71,8 @@ void perform_axe_attack(int current_x) { // Play hit sound based on enemy type if (get_bandit_at(current_x) != null) { play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, current_x, BANDIT_SOUND_VOLUME_STEP); + } else if (get_boar_at(current_x) != null) { + play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, current_x, BOAR_SOUND_VOLUME_STEP); } else { play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, current_x, ZOMBIE_SOUND_VOLUME_STEP); } @@ -131,6 +143,7 @@ void release_sling_attack(int player_x) { int target_x = -1; bool hit_bandit = false; bool hit_flying_creature = false; + bool hit_boar = false; // Priority: Find nearest enemy (bandit or zombie) first for (int dist = 1; dist <= SLING_RANGE; dist++) { @@ -144,6 +157,14 @@ void release_sling_attack(int player_x) { hit_bandit = true; break; } + + // Then check for boar + GameBoar@ boar = get_boar_at(check_x); + if (boar != null) { + target_x = check_x; + hit_boar = true; + break; + } // Then check for zombie Zombie@ zombie = get_zombie_at(check_x); @@ -190,6 +211,10 @@ void release_sling_attack(int player_x) { damage_bandit_at(target_x, damage); 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_boar) { + damage_boar_at(target_x, damage); + 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, BOAR_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); diff --git a/src/constants.nvgt b/src/constants.nvgt index afcbae9..34fa446 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -32,20 +32,12 @@ const int SLING_DAMAGE_MIN = 5; const int SLING_DAMAGE_MAX = 8; 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) +// Bow settings +const int BOW_DAMAGE_MIN = 6; +const int BOW_DAMAGE_MAX = 9; +const int BOW_RANGE = 12; +const int ARROWS_PER_CRAFT = 12; +const int ARROW_CAPACITY_PER_QUIVER = 12; // Zombie settings const int ZOMBIE_HEALTH = 12; @@ -60,6 +52,21 @@ const int ZOMBIE_FOOTSTEP_MAX_DISTANCE = 5; const float ZOMBIE_SOUND_VOLUME_STEP = 3.0; const int ZOMBIE_ATTACK_MAX_HEIGHT = 6; +// Boar settings +const int BOAR_HEALTH = 4; +const int BOAR_MAX_COUNT = 3; +const int BOAR_MOVE_INTERVAL_MIN = 800; +const int BOAR_MOVE_INTERVAL_MAX = 1500; +const int BOAR_ATTACK_INTERVAL = 1500; +const int BOAR_DAMAGE_MIN = 1; +const int BOAR_DAMAGE_MAX = 3; +const int BOAR_SOUND_MIN_DELAY = 3000; +const int BOAR_SOUND_MAX_DELAY = 6000; +const int BOAR_FOOTSTEP_MAX_DISTANCE = 5; +const float BOAR_SOUND_VOLUME_STEP = 3.0; +const int BOAR_SIGHT_RANGE = 4; +const int BOAR_CHARGE_SPEED = 500; // ms per tile when charging + // Barricade configuration const int BARRICADE_BASE_HEALTH = 100; const int BARRICADE_MAX_HEALTH = 500; @@ -188,10 +195,10 @@ const int WIND_GUST_MIN_DELAY = 30000; // Min 30 seconds between gusts const int WIND_GUST_MAX_DELAY = 60000; // Max 60 seconds between gusts 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 +const int THUNDER_MOVEMENT_SPEED = 250; // ms per tile movement (faster roll across sky) +const float THUNDER_SOUND_VOLUME_STEP = 0.5; // Gentler volume falloff +const int THUNDER_SPAWN_DISTANCE_MIN = 0; // Min distance from player +const int THUNDER_SPAWN_DISTANCE_MAX = 20; // Max distance from player const int CHANCE_CLEAR_TO_WINDY = 15; const int CHANCE_CLEAR_TO_RAINY = 6; const int CHANCE_CLEAR_TO_STORMY = 5; @@ -202,3 +209,27 @@ const int CHANCE_RAINY_STAY = 40; const int CHANCE_RAINY_TO_STORMY = 35; const int CHANCE_STORMY_STAY = 40; const int CHANCE_STORMY_TO_RAINY = 35; + +// Weather States +const int WEATHER_CLEAR = 0; +const int WEATHER_WINDY = 1; +const int WEATHER_RAINY = 2; +const int WEATHER_STORMY = 3; + +// Weather 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; + +const string RAIN_SOUND = "sounds/nature/rain.ogg"; + +// Environment / Fall Damage +const int SAFE_FALL_HEIGHT = 10; +const int FALL_DAMAGE_MIN = 0; +const int FALL_DAMAGE_MAX = 4; + +// Base Automation +const int RESIDENT_SLING_COOLDOWN = 4000; // 4 seconds between shots +const int RESIDENT_COLLECTION_CHANCE = 10; // 10% chance per basket per hour + diff --git a/src/crafting.nvgt b/src/crafting.nvgt index ebd476f..ab9d70a 100644 --- a/src/crafting.nvgt +++ b/src/crafting.nvgt @@ -949,8 +949,8 @@ void butcher_small_game() { // Check for knife if (inv_knives < 1) missing += "Stone Knife "; - // Check for small game - if (inv_small_game < 1) missing += "Small Game "; + // Check for small game or boar + if (inv_small_game < 1 && inv_boar_carcasses < 1) missing += "Game "; // Check for fire within 3 tiles (can hear it) WorldFire@ fire = get_fire_within_range(x, 3); @@ -970,15 +970,26 @@ void butcher_small_game() { } simulate_crafting(1); - string game_type = inv_small_game_types[0]; - inv_small_game_types.remove_at(0); - inv_small_game--; + string game_type = ""; + if (inv_boar_carcasses > 0) { + game_type = "boar carcass"; + inv_boar_carcasses--; + } else { + game_type = inv_small_game_types[0]; + inv_small_game_types.remove_at(0); + inv_small_game--; + } 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 if (game_type == "boar carcass") { + inv_meat += random(2, 3); + inv_skins += 3; + inv_sinew += 2; + screen_reader_speak("Butchered boar. Got meat, 3 skins, and 2 sinew.", true); } else { inv_meat++; inv_skins++; diff --git a/src/environment.nvgt b/src/environment.nvgt index 2e4c738..c1d47c9 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -2,9 +2,6 @@ // 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 diff --git a/src/inventory_items.nvgt b/src/inventory_items.nvgt index aee93e1..954b115 100644 --- a/src/inventory_items.nvgt +++ b/src/inventory_items.nvgt @@ -13,6 +13,12 @@ int inv_skins = 0; int inv_feathers = 0; int inv_down = 0; int inv_incense = 0; +int inv_bows = 0; +int inv_arrows = 0; +int inv_quivers = 0; +int inv_bowstrings = 0; +int inv_sinew = 0; +int inv_boar_carcasses = 0; int inv_spears = 0; int inv_snares = 0; @@ -43,6 +49,12 @@ int storage_skins = 0; int storage_feathers = 0; int storage_down = 0; int storage_incense = 0; +int storage_bows = 0; +int storage_arrows = 0; +int storage_quivers = 0; +int storage_bowstrings = 0; +int storage_sinew = 0; +int storage_boar_carcasses = 0; int storage_spears = 0; int storage_snares = 0; @@ -63,11 +75,13 @@ int storage_skin_pouches = 0; bool spear_equipped = false; bool axe_equipped = false; bool sling_equipped = false; +bool bow_equipped = false; int[] quick_slots; const int EQUIP_NONE = -1; const int EQUIP_SPEAR = 0; const int EQUIP_AXE = 1; const int EQUIP_SLING = 2; +const int EQUIP_BOW = 9; // Next available ID const int EQUIP_HAT = 3; const int EQUIP_GLOVES = 4; const int EQUIP_PANTS = 5; @@ -101,6 +115,12 @@ const int ITEM_CLAY_POTS = 23; const int ITEM_FEATHERS = 24; const int ITEM_DOWN = 25; const int ITEM_INCENSE = 26; +const int ITEM_BOWS = 27; +const int ITEM_ARROWS = 28; +const int ITEM_QUIVERS = 29; +const int ITEM_BOWSTRINGS = 30; +const int ITEM_SINEW = 31; +const int ITEM_BOAR_CARCASSES = 32; const int HAT_MAX_HEALTH_BONUS = 1; const int GLOVES_MAX_HEALTH_BONUS = 1; const int PANTS_MAX_HEALTH_BONUS = 3; @@ -128,10 +148,18 @@ int get_personal_stack_limit() { return limit; } +int get_arrow_limit() { + // Quiver required to hold arrows + // Each quiver holds 12 arrows + if (inv_quivers == 0) return 0; + return inv_quivers * ARROW_CAPACITY_PER_QUIVER; +} + string get_equipment_name(int equip_type) { if (equip_type == EQUIP_SPEAR) return "Spear"; if (equip_type == EQUIP_AXE) return "Stone Axe"; if (equip_type == EQUIP_SLING) return "Sling"; + if (equip_type == EQUIP_BOW) return "Bow"; if (equip_type == EQUIP_HAT) return "Skin Hat"; if (equip_type == EQUIP_GLOVES) return "Skin Gloves"; if (equip_type == EQUIP_PANTS) return "Skin Pants"; @@ -145,6 +173,7 @@ bool equipment_available(int equip_type) { if (equip_type == EQUIP_SPEAR) return inv_spears > 0; if (equip_type == EQUIP_AXE) return inv_axes > 0; if (equip_type == EQUIP_SLING) return inv_slings > 0; + if (equip_type == EQUIP_BOW) return inv_bows > 0; if (equip_type == EQUIP_HAT) return inv_skin_hats > 0; if (equip_type == EQUIP_GLOVES) return inv_skin_gloves > 0; if (equip_type == EQUIP_PANTS) return inv_skin_pants > 0; @@ -155,10 +184,11 @@ bool equipment_available(int equip_type) { } void equip_equipment_type(int equip_type) { - if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || equip_type == EQUIP_SLING) { + if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || equip_type == EQUIP_SLING || equip_type == EQUIP_BOW) { spear_equipped = (equip_type == EQUIP_SPEAR); axe_equipped = (equip_type == EQUIP_AXE); sling_equipped = (equip_type == EQUIP_SLING); + bow_equipped = (equip_type == EQUIP_BOW); return; } @@ -174,6 +204,7 @@ bool equipment_is_equipped(int equip_type) { if (equip_type == EQUIP_SPEAR) return spear_equipped; if (equip_type == EQUIP_AXE) return axe_equipped; if (equip_type == EQUIP_SLING) return sling_equipped; + if (equip_type == EQUIP_BOW) return bow_equipped; if (equip_type == EQUIP_HAT) return equipped_head == EQUIP_HAT; if (equip_type == EQUIP_TUNIC) return equipped_torso == EQUIP_TUNIC; if (equip_type == EQUIP_GLOVES) return equipped_hands == EQUIP_GLOVES; @@ -190,6 +221,8 @@ void unequip_equipment_type(int equip_type) { axe_equipped = false; } else if (equip_type == EQUIP_SLING) { sling_equipped = false; + } else if (equip_type == EQUIP_BOW) { + bow_equipped = false; } else if (equip_type == EQUIP_HAT && equipped_head == EQUIP_HAT) { equipped_head = EQUIP_NONE; } else if (equip_type == EQUIP_TUNIC && equipped_torso == EQUIP_TUNIC) { @@ -286,6 +319,12 @@ int get_personal_count(int item_type) { 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_BOWS) return inv_bows; + if (item_type == ITEM_ARROWS) return inv_arrows; + if (item_type == ITEM_QUIVERS) return inv_quivers; + if (item_type == ITEM_BOWSTRINGS) return inv_bowstrings; + if (item_type == ITEM_SINEW) return inv_sinew; + if (item_type == ITEM_BOAR_CARCASSES) return inv_boar_carcasses; if (item_type == ITEM_SPEARS) return inv_spears; if (item_type == ITEM_SLINGS) return inv_slings; if (item_type == ITEM_AXES) return inv_axes; @@ -317,6 +356,12 @@ int get_storage_count(int item_type) { 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_BOWS) return storage_bows; + if (item_type == ITEM_ARROWS) return storage_arrows; + if (item_type == ITEM_QUIVERS) return storage_quivers; + if (item_type == ITEM_BOWSTRINGS) return storage_bowstrings; + if (item_type == ITEM_SINEW) return storage_sinew; + if (item_type == ITEM_BOAR_CARCASSES) return storage_boar_carcasses; if (item_type == ITEM_SPEARS) return storage_spears; if (item_type == ITEM_SLINGS) return storage_slings; if (item_type == ITEM_AXES) return storage_axes; @@ -348,6 +393,12 @@ string get_item_label(int item_type) { 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_BOWS) return "bows"; + if (item_type == ITEM_ARROWS) return "arrows"; + if (item_type == ITEM_QUIVERS) return "quivers"; + if (item_type == ITEM_BOWSTRINGS) return "bowstrings"; + if (item_type == ITEM_SINEW) return "sinew"; + if (item_type == ITEM_BOAR_CARCASSES) return "boar carcasses"; if (item_type == ITEM_SPEARS) return "spears"; if (item_type == ITEM_SLINGS) return "slings"; if (item_type == ITEM_AXES) return "axes"; @@ -368,11 +419,7 @@ string get_item_label(int item_type) { string format_favor(double value) { if (value < 0) value = 0; - int scaled = int(value * 100 + 0.5); - int whole = scaled / 100; - int frac = scaled % 100; - string frac_text = (frac < 10) ? "0" + frac : "" + frac; - return "" + whole + "." + frac_text; + return "" + int(value); } string get_item_label_singular(int item_type) { @@ -388,6 +435,12 @@ string get_item_label_singular(int item_type) { 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_BOWS) return "bow"; + if (item_type == ITEM_ARROWS) return "arrow"; + if (item_type == ITEM_QUIVERS) return "quiver"; + if (item_type == ITEM_BOWSTRINGS) return "bowstring"; + if (item_type == ITEM_SINEW) return "piece of sinew"; + if (item_type == ITEM_BOAR_CARCASSES) return "boar carcass"; if (item_type == ITEM_SPEARS) return "spear"; if (item_type == ITEM_SLINGS) return "sling"; if (item_type == ITEM_AXES) return "axe"; @@ -419,6 +472,12 @@ double get_item_favor_value(int item_type) { 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_BOWS) return 2.50; + if (item_type == ITEM_ARROWS) return 0.05; + if (item_type == ITEM_QUIVERS) return 1.50; + if (item_type == ITEM_BOWSTRINGS) return 0.20; + if (item_type == ITEM_SINEW) return 0.10; + if (item_type == ITEM_BOAR_CARCASSES) return 1.50; if (item_type == ITEM_SPEARS) return 1.00; if (item_type == ITEM_SLINGS) return 2.00; if (item_type == ITEM_AXES) return 1.50; @@ -441,6 +500,7 @@ string get_equipped_weapon_name() { if (spear_equipped) return "Spear"; if (axe_equipped) return "Stone Axe"; if (sling_equipped) return "Sling"; + if (bow_equipped) return "Bow"; return "None"; } @@ -454,6 +514,7 @@ void cleanup_equipment_after_inventory_change() { if (inv_spears <= 0) spear_equipped = false; if (inv_axes <= 0) axe_equipped = false; if (inv_slings <= 0) sling_equipped = false; + if (inv_bows <= 0) bow_equipped = false; if (inv_skin_hats <= 0) equipped_head = EQUIP_NONE; if (inv_skin_gloves <= 0) equipped_hands = EQUIP_NONE; if (inv_skin_pants <= 0) equipped_legs = EQUIP_NONE; diff --git a/src/weather.nvgt b/src/weather.nvgt index e610a28..37e9ecc 100644 --- a/src/weather.nvgt +++ b/src/weather.nvgt @@ -2,18 +2,6 @@ // Provides ambient wind, rain, and thunder effects // Tunable constants are in src/constants.nvgt -// 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; - // State variables int weather_state = WEATHER_CLEAR; int wind_intensity = INTENSITY_NONE; @@ -37,26 +25,30 @@ timer rain_fade_timer; // Thunder object state class ThunderStrike { int position; - int direction; // -1 = moving west, 1 = moving east + int direction; // -1 = moving west, 1 = moving east, 0 = stationary int sound_handle; timer movement_timer; + int movement_speed; - ThunderStrike(int pos, int dir, int handle) { + ThunderStrike(int pos, int dir, int handle, int speed) { position = pos; direction = dir; sound_handle = handle; + movement_speed = speed; movement_timer.restart(); } void update() { - if (movement_timer.elapsed >= THUNDER_MOVEMENT_SPEED) { + if (direction == 0) return; // Stationary thunder + + if (movement_timer.elapsed >= 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); + + // Update sound position + if (sound_handle != -1 && p.sound_is_active(sound_handle)) { + p.update_sound_1d(sound_handle, position); + } } } @@ -83,8 +75,6 @@ string[] thunder_sounds = { "sounds/nature/thunder_high.ogg" }; -const string RAIN_SOUND = "sounds/nature/rain.ogg"; - void init_weather() { weather_state = WEATHER_CLEAR; wind_intensity = INTENSITY_NONE; @@ -385,15 +375,23 @@ void spawn_thunder() { // 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); + + // Determine movement: 20% stationary, 80% moving + int direction = 0; + if (random(1, 100) > 20) { + direction = random(0, 1) == 0 ? -1 : 1; + } + + // Random speed: 50ms (fast rip) to 600ms (slow roll) + int speed = random(50, 600); + + int thunder_pos = x + (distance * ((direction == 0) ? (random(0, 1) == 0 ? -1 : 1) : 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); + ThunderStrike@ strike = ThunderStrike(thunder_pos, direction, handle, speed); active_thunder.insert_last(strike); } } diff --git a/src/world_state.nvgt b/src/world_state.nvgt index 02ad9d3..7eb840a 100644 --- a/src/world_state.nvgt +++ b/src/world_state.nvgt @@ -10,6 +10,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"}; +string[] boar_sounds = {"sounds/game/boar.ogg"}; class Zombie { int position; @@ -83,6 +84,42 @@ class Bandit { } Bandit@[] bandits; +class GameBoar { + int position; + int health; + int sound_handle; + timer move_timer; + timer sound_timer; + timer attack_timer; + int next_move_delay; + int next_sound_delay; + string voice_sound; + string state; // "wandering" or "charging" + int area_start; + int area_end; + int wander_direction; // -1, 0, 1 + + GameBoar(int pos, int start, int end) { + position = pos; + area_start = start; + area_end = end; + health = BOAR_HEALTH; + sound_handle = -1; + state = "wandering"; + wander_direction = 0; + + voice_sound = boar_sounds[random(0, boar_sounds.length() - 1)]; + + move_timer.restart(); + sound_timer.restart(); + attack_timer.restart(); + + next_move_delay = random(BOAR_MOVE_INTERVAL_MIN, BOAR_MOVE_INTERVAL_MAX); + next_sound_delay = random(BOAR_SOUND_MIN_DELAY, BOAR_SOUND_MAX_DELAY); + } +} +GameBoar@[] boars; + class FlyingCreatureConfig { string id; string drop_type; @@ -247,6 +284,15 @@ 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); } + if (drop.type == "boar carcass") { + if (inv_boar_carcasses >= get_personal_stack_limit()) { + screen_reader_speak("You can't carry any more boar carcasses.", true); + return false; + } + inv_boar_carcasses++; + screen_reader_speak("Picked up boar carcass.", true); + return true; + } screen_reader_speak("Picked up " + drop.type + ".", true); return true; } @@ -960,6 +1006,175 @@ Bandit@ get_bandit_at(int pos) { return null; } +// Boar Functions +void clear_boars() { + if (boars.length() == 0) return; + + for (uint i = 0; i < boars.length(); i++) { + if (boars[i].sound_handle != -1) { + p.destroy_sound(boars[i].sound_handle); + boars[i].sound_handle = -1; + } + } + boars.resize(0); +} + +GameBoar@ get_boar_at(int pos) { + for (uint i = 0; i < boars.length(); i++) { + if (boars[i].position == pos) { + return @boars[i]; + } + } + return null; +} + +void spawn_boar(int expansion_start, int expansion_end) { + int spawn_x = -1; + // Try to find a valid spawn position in grass/snow (expanded area) + for (int attempts = 0; attempts < 20; attempts++) { + int candidate = random(expansion_start, expansion_end); + + // Don't spawn too close to base (keep away from BASE_END) + if (candidate <= BASE_END + 5) continue; + + if (get_boar_at(candidate) == null) { + spawn_x = candidate; + break; + } + } + if (spawn_x == -1) return; // Failed to find spot + + GameBoar@ b = GameBoar(spawn_x, expansion_start, expansion_end); + boars.insert_last(b); + b.sound_handle = play_creature_voice(b.voice_sound, x, spawn_x, BOAR_SOUND_VOLUME_STEP); +} + +bool can_boar_attack_player(GameBoar@ boar) { + if (player_health <= 0) return false; + + // Check if player is on ground (boars can't fly/climb) + if (y > 0) return false; + + if (abs(boar.position - x) > 1) return false; + + return true; +} + +bool try_attack_player_boar(GameBoar@ boar) { + if (!can_boar_attack_player(boar)) return false; + + if (boar.attack_timer.elapsed < BOAR_ATTACK_INTERVAL) return false; + + boar.attack_timer.restart(); + + // Attack! + // TODO: Add specific boar attack sound? For now re-use zombie hit as generic impact + play_creature_attack_sound("sounds/enemies/zombie_hits_player.ogg", x, boar.position, BOAR_SOUND_VOLUME_STEP); + + int damage = random(BOAR_DAMAGE_MIN, BOAR_DAMAGE_MAX); + player_health -= damage; + if (player_health < 0) player_health = 0; + + return true; +} + +void update_boar(GameBoar@ boar) { + // Sound logic + if (boar.sound_timer.elapsed > boar.next_sound_delay) { + boar.sound_timer.restart(); + boar.next_sound_delay = random(BOAR_SOUND_MIN_DELAY, BOAR_SOUND_MAX_DELAY); + // Only play if wandering or occasionally while charging + boar.sound_handle = play_creature_voice(boar.voice_sound, x, boar.position, BOAR_SOUND_VOLUME_STEP); + } + + // Combat logic + if (try_attack_player_boar(boar)) { + return; + } + + // Movement logic + int move_speed = (boar.state == "charging") ? BOAR_CHARGE_SPEED : boar.next_move_delay; + + if (boar.move_timer.elapsed < move_speed) return; + boar.move_timer.restart(); + if (boar.state == "wandering") { + boar.next_move_delay = random(BOAR_MOVE_INTERVAL_MIN, BOAR_MOVE_INTERVAL_MAX); + } + + // Detection Logic + int dist_to_player = x - boar.position; + int abs_dist = abs(dist_to_player); + + // If player is close, on ground, and boar can see them -> Charge + if (y == 0 && abs_dist <= BOAR_SIGHT_RANGE && x > BASE_END) { + boar.state = "charging"; + } else { + boar.state = "wandering"; + } + + if (boar.state == "charging") { + int dir = (dist_to_player > 0) ? 1 : -1; + int target = boar.position + dir; + + // Don't leave area or enter base + if (target >= boar.area_start && target <= boar.area_end && target > BASE_END) { + boar.position = target; + play_creature_footstep(x, boar.position, BASE_END, GRASS_END, BOAR_FOOTSTEP_MAX_DISTANCE, BOAR_SOUND_VOLUME_STEP); + } + } else { + // Wandering + if (random(1, 100) <= 20) { + boar.wander_direction = random(-1, 1); + } + + if (boar.wander_direction != 0) { + int target = boar.position + boar.wander_direction; + // Don't leave area or enter base + if (target >= boar.area_start && target <= boar.area_end && target > BASE_END) { + boar.position = target; + play_creature_footstep(x, boar.position, BASE_END, GRASS_END, BOAR_FOOTSTEP_MAX_DISTANCE, BOAR_SOUND_VOLUME_STEP); + } else { + boar.wander_direction = -boar.wander_direction; // Turn around + } + } + } +} + +void update_boars() { + // Only spawn if map is expanded + if (expanded_area_start != -1) { + while (boars.length() < BOAR_MAX_COUNT) { + spawn_boar(expanded_area_start, expanded_area_end); + } + } + + for (uint i = 0; i < boars.length(); i++) { + update_boar(boars[i]); + } +} + +bool damage_boar_at(int pos, int damage) { + for (uint i = 0; i < boars.length(); i++) { + if (boars[i].position == pos) { + boars[i].health -= damage; + if (boars[i].health <= 0) { + if (boars[i].sound_handle != -1) { + p.destroy_sound(boars[i].sound_handle); + boars[i].sound_handle = -1; + } + play_creature_death_sound("sounds/game/game_falls.ogg", x, pos, BOAR_SOUND_VOLUME_STEP); + + // Drop carcass + add_world_drop(pos, "boar carcass"); + + boars.remove_at(i); + } + return true; + } + } + return false; +} + void spawn_bandit(int expansion_start, int expansion_end) { int spawn_x = -1; for (int attempts = 0; attempts < 20; attempts++) {