From 19252a8566df35884499f7f1562ae0d21959fc0c Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Wed, 21 Jan 2026 18:30:47 -0500 Subject: [PATCH] Continue refactor splitting into smaller files. --- draugnorak.nvgt | 2 + src/combat.nvgt | 2 +- src/enemies/bandit.nvgt | 297 +++++++++++++++++++++ src/enemies/ground_game.nvgt | 221 +++++++++++++++ src/world_state.nvgt | 504 +---------------------------------- 5 files changed, 523 insertions(+), 503 deletions(-) create mode 100644 src/enemies/bandit.nvgt create mode 100644 src/enemies/ground_game.nvgt diff --git a/draugnorak.nvgt b/draugnorak.nvgt index 1cef639..cf8d0c6 100644 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -10,6 +10,8 @@ sound_pool p(100); #include "src/creature_death.nvgt" #include "src/world_object_base.nvgt" #include "src/enemies/undead.nvgt" +#include "src/enemies/bandit.nvgt" +#include "src/enemies/ground_game.nvgt" #include "src/world_state.nvgt" #include "src/ui.nvgt" #include "src/inventory.nvgt" diff --git a/src/combat.nvgt b/src/combat.nvgt index e191345..8b71c35 100644 --- a/src/combat.nvgt +++ b/src/combat.nvgt @@ -159,7 +159,7 @@ void release_sling_attack(int player_x) { } // Then check for boar - GameBoar@ boar = get_boar_at(check_x); + GroundGame@ boar = get_boar_at(check_x); if (boar != null) { target_x = check_x; hit_boar = true; diff --git a/src/enemies/bandit.nvgt b/src/enemies/bandit.nvgt new file mode 100644 index 0000000..a984ee7 --- /dev/null +++ b/src/enemies/bandit.nvgt @@ -0,0 +1,297 @@ +// Bandit enemies +// Hostile humans that spawn during invasions and wander in expanded areas + +string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"}; + +class Bandit { + int position; + int health; + string alert_sound; + string weapon_type; // "spear" or "axe" + int sound_handle; + timer move_timer; + timer alert_timer; + timer attack_timer; + int next_alert_delay; + int move_interval; + + // Wandering behavior properties + string behavior_state; // "aggressive" or "wandering" + int wander_direction; // -1, 0, or 1 + timer wander_direction_timer; + int wander_direction_change_interval; + + Bandit(int pos, int expansion_start, int expansion_end) { + // Spawn somewhere in the expanded area + position = random(expansion_start, expansion_end); + health = BANDIT_HEALTH; + sound_handle = -1; + + // Choose random alert sound + int sound_index = random(0, bandit_sounds.length() - 1); + alert_sound = bandit_sounds[sound_index]; + + // Choose random weapon + weapon_type = (random(0, 1) == 0) ? "spear" : "axe"; + + // Random movement speed within range + move_interval = random(BANDIT_MOVE_INTERVAL_MIN, BANDIT_MOVE_INTERVAL_MAX); + + move_timer.restart(); + alert_timer.restart(); + attack_timer.restart(); + next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY); + + // Initialize wandering behavior (start aggressive during invasion) + behavior_state = "aggressive"; + wander_direction = 0; + wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); + wander_direction_timer.restart(); + } +} +Bandit@[] bandits; + +void clear_bandits() { + if (bandits.length() == 0) return; + + for (uint i = 0; i < bandits.length(); i++) { + if (bandits[i].sound_handle != -1) { + p.destroy_sound(bandits[i].sound_handle); + bandits[i].sound_handle = -1; + } + } + bandits.resize(0); +} + +Bandit@ get_bandit_at(int pos) { + for (uint i = 0; i < bandits.length(); i++) { + if (bandits[i].position == pos) { + return @bandits[i]; + } + } + return null; +} + +void spawn_bandit(int expansion_start, int expansion_end) { + int spawn_x = -1; + for (int attempts = 0; attempts < 20; attempts++) { + int candidate = random(expansion_start, expansion_end); + if (get_bandit_at(candidate) == null) { + spawn_x = candidate; + break; + } + } + if (spawn_x == -1) { + spawn_x = random(expansion_start, expansion_end); + } + + Bandit@ b = Bandit(spawn_x, expansion_start, expansion_end); + bandits.insert_last(b); + b.sound_handle = play_creature_voice(b.alert_sound, x, spawn_x, BANDIT_SOUND_VOLUME_STEP); +} + +bool can_bandit_attack_player(Bandit@ bandit) { + if (player_health <= 0) { + return false; + } + + // Cannot attack player if barricade is up and player is in base + if (barricade_health > 0 && x <= BASE_END) { + return false; + } + + if (abs(bandit.position - x) > 1) { + return false; + } + + return y <= BANDIT_ATTACK_MAX_HEIGHT; +} + +bool try_attack_player_bandit(Bandit@ bandit) { + if (!can_bandit_attack_player(bandit)) { + return false; + } + if (bandit.attack_timer.elapsed < BANDIT_ATTACK_INTERVAL) { + return false; + } + + bandit.attack_timer.restart(); + + // Play weapon swing sound based on bandit's weapon + if (bandit.weapon_type == "spear") { + play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); + } else if (bandit.weapon_type == "axe") { + 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); + player_health -= damage; + if (player_health < 0) { + player_health = 0; + } + + // Play hit sound + if (bandit.weapon_type == "spear") { + play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); + } else if (bandit.weapon_type == "axe") { + play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); + } + + return true; +} + +void try_attack_barricade_bandit(Bandit@ bandit) { + if (barricade_health <= 0) return; + if (bandit.attack_timer.elapsed < BANDIT_ATTACK_INTERVAL) return; + + bandit.attack_timer.restart(); + + // Bandits do 1-2 damage to barricade + int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX); + barricade_health -= damage; + if (barricade_health < 0) barricade_health = 0; + + // Play weapon swing sound + if (bandit.weapon_type == "spear") { + 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") { + 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 + if (can_residents_defend()) { + int counterDamage = perform_resident_defense(); + if (counterDamage > 0) { + bandit.health -= counterDamage; + if (bandit.health <= 0 && x <= BASE_END) { + speak_with_history("Residents killed an attacking bandit.", true); + } + } + } + + if (barricade_health == 0) { + notify("The barricade has fallen!"); + } +} + +void update_bandit(Bandit@ bandit) { + // Play alert sound at intervals + if (bandit.alert_timer.elapsed > bandit.next_alert_delay) { + bandit.alert_timer.restart(); + bandit.next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY); + bandit.sound_handle = play_creature_voice(bandit.alert_sound, x, bandit.position, BANDIT_SOUND_VOLUME_STEP); + } + + if (try_attack_player_bandit(bandit)) { + return; + } + + if (bandit.move_timer.elapsed < bandit.move_interval) return; + bandit.move_timer.restart(); + + // If barricade is up and bandit is at the edge of base, attack barricade + if (barricade_health > 0 && bandit.position == BASE_END + 1) { + try_attack_barricade_bandit(bandit); + return; + } + + // State-based behavior + if (bandit.behavior_state == "wandering") { + // Check if player is within detection radius + int distance = abs(bandit.position - x); + if (distance <= BANDIT_DETECTION_RADIUS) { + // Player detected! Switch to aggressive + bandit.behavior_state = "aggressive"; + } else { + // Continue wandering + if (bandit.wander_direction_timer.elapsed > bandit.wander_direction_change_interval) { + // Time to change direction + bandit.wander_direction = random(-1, 1); + bandit.wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); + bandit.wander_direction_timer.restart(); + } + + // Move in wander direction (if not 0) + if (bandit.wander_direction != 0) { + int target_x = bandit.position + bandit.wander_direction; + + // Check bounds + if (target_x >= 0 && target_x < MAP_SIZE) { + // Don't wander into base if barricade is up + if (target_x <= BASE_END && barricade_health > 0) { + // Change direction instead + bandit.wander_direction = -bandit.wander_direction; + } else { + bandit.position = target_x; + play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); + } + } else { + // Hit map boundary, reverse direction + bandit.wander_direction = -bandit.wander_direction; + } + } + return; + } + } + + // Aggressive behavior (original logic) + if (bandit.behavior_state == "aggressive") { + // Move toward player + int direction = 0; + if (x > BASE_END) { + // Player is outside base, move toward them + if (x > bandit.position) { + direction = 1; + } else if (x < bandit.position) { + direction = -1; + } else { + return; + } + } else { + // Player is in base, move toward base edge + if (bandit.position > BASE_END + 1) { + direction = -1; + } else { + return; // Already at base edge + } + } + + int target_x = bandit.position + direction; + if (target_x < 0 || target_x >= MAP_SIZE) return; + + // Don't enter base if barricade is up + if (target_x <= BASE_END && barricade_health > 0) { + try_attack_barricade_bandit(bandit); + return; + } + + bandit.position = target_x; + play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); + } +} + +void update_bandits() { + for (uint i = 0; i < bandits.length(); i++) { + update_bandit(bandits[i]); + } +} + +bool damage_bandit_at(int pos, int damage) { + for (uint i = 0; i < bandits.length(); i++) { + if (bandits[i].position == pos) { + bandits[i].health -= damage; + if (bandits[i].health <= 0) { + if (bandits[i].sound_handle != -1) { + p.destroy_sound(bandits[i].sound_handle); + bandits[i].sound_handle = -1; + } + play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, BANDIT_SOUND_VOLUME_STEP); + bandits.remove_at(i); + } + return true; + } + } + return false; +} diff --git a/src/enemies/ground_game.nvgt b/src/enemies/ground_game.nvgt new file mode 100644 index 0000000..b58f3ec --- /dev/null +++ b/src/enemies/ground_game.nvgt @@ -0,0 +1,221 @@ +// Ground game animals (boars, mountain goats, rams, etc.) +// Currently only boars are implemented + +string[] ground_game_boar_sounds = {"sounds/game/boar.ogg"}; + +class GroundGame { + 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 + string animal_type; // "boar", future: "mountain_goat", "ram", etc. + + GroundGame(int pos, int start, int end, string type = "boar") { + position = pos; + area_start = start; + area_end = end; + animal_type = type; + health = BOAR_HEALTH; + sound_handle = -1; + state = "wandering"; + wander_direction = 0; + + voice_sound = ground_game_boar_sounds[random(0, ground_game_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); + } +} +GroundGame@[] ground_games; + +void clear_ground_games() { + if (ground_games.length() == 0) return; + + for (uint i = 0; i < ground_games.length(); i++) { + if (ground_games[i].sound_handle != -1) { + p.destroy_sound(ground_games[i].sound_handle); + ground_games[i].sound_handle = -1; + } + } + ground_games.resize(0); +} + +GroundGame@ get_ground_game_at(int pos) { + for (uint i = 0; i < ground_games.length(); i++) { + if (ground_games[i].position == pos) { + return @ground_games[i]; + } + } + return null; +} + +void spawn_ground_game(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_ground_game_at(candidate) == null) { + spawn_x = candidate; + break; + } + } + if (spawn_x == -1) return; // Failed to find spot + + GroundGame@ b = GroundGame(spawn_x, expansion_start, expansion_end, "boar"); + ground_games.insert_last(b); + b.sound_handle = play_creature_voice(b.voice_sound, x, spawn_x, BOAR_SOUND_VOLUME_STEP); +} + +bool can_ground_game_attack_player(GroundGame@ game) { + if (player_health <= 0) return false; + + // Check if player is on ground (ground game can't fly/climb) + if (y > 0) return false; + + if (abs(game.position - x) > 1) return false; + + return true; +} + +bool try_attack_player_ground_game(GroundGame@ game) { + if (!can_ground_game_attack_player(game)) return false; + + if (game.attack_timer.elapsed < BOAR_ATTACK_INTERVAL) return false; + + game.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, game.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_ground_game(GroundGame@ game) { + // Sound logic + if (game.sound_timer.elapsed > game.next_sound_delay) { + game.sound_timer.restart(); + game.next_sound_delay = random(BOAR_SOUND_MIN_DELAY, BOAR_SOUND_MAX_DELAY); + // Only play if wandering or occasionally while charging + game.sound_handle = play_creature_voice(game.voice_sound, x, game.position, BOAR_SOUND_VOLUME_STEP); + } + + // Combat logic + if (try_attack_player_ground_game(game)) { + return; + } + + // Movement logic + int move_speed = (game.state == "charging") ? BOAR_CHARGE_SPEED : game.next_move_delay; + + if (game.move_timer.elapsed < move_speed) return; + game.move_timer.restart(); + if (game.state == "wandering") { + game.next_move_delay = random(BOAR_MOVE_INTERVAL_MIN, BOAR_MOVE_INTERVAL_MAX); + } + + // Detection Logic + int dist_to_player = x - game.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) { + game.state = "charging"; + } else { + game.state = "wandering"; + } + + if (game.state == "charging") { + int dir = (dist_to_player > 0) ? 1 : -1; + int target = game.position + dir; + + // Don't leave area or enter base + if (target >= game.area_start && target <= game.area_end && target > BASE_END) { + game.position = target; + play_creature_footstep(x, game.position, BASE_END, GRASS_END, BOAR_FOOTSTEP_MAX_DISTANCE, BOAR_SOUND_VOLUME_STEP); + } + } else { + // Wandering + if (random(1, 100) <= 20) { + game.wander_direction = random(-1, 1); + } + + if (game.wander_direction != 0) { + int target = game.position + game.wander_direction; + // Don't leave area or enter base + if (target >= game.area_start && target <= game.area_end && target > BASE_END) { + game.position = target; + play_creature_footstep(x, game.position, BASE_END, GRASS_END, BOAR_FOOTSTEP_MAX_DISTANCE, BOAR_SOUND_VOLUME_STEP); + } else { + game.wander_direction = -game.wander_direction; // Turn around + } + } + } +} + +void update_ground_games() { + for (uint i = 0; i < ground_games.length(); i++) { + update_ground_game(ground_games[i]); + } +} + +void attempt_hourly_ground_game_spawn() { + if (expanded_area_start == -1) return; + if (ground_games.length() >= BOAR_MAX_COUNT) return; + + if (random(1, 100) <= BOAR_SPAWN_CHANCE_PER_HOUR) { + spawn_ground_game(expanded_area_start, expanded_area_end); + } +} + +bool damage_ground_game_at(int pos, int damage) { + for (uint i = 0; i < ground_games.length(); i++) { + if (ground_games[i].position == pos) { + ground_games[i].health -= damage; + if (ground_games[i].health <= 0) { + if (ground_games[i].sound_handle != -1) { + p.destroy_sound(ground_games[i].sound_handle); + ground_games[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"); + + ground_games.remove_at(i); + } + return true; + } + } + return false; +} + +// Backward compatibility aliases (to be removed after refactoring) +GroundGame@[]@ boars = @ground_games; +GroundGame@ get_boar_at(int pos) { return get_ground_game_at(pos); } +void clear_boars() { clear_ground_games(); } +void spawn_boar(int expansion_start, int expansion_end) { spawn_ground_game(expansion_start, expansion_end); } +void update_boars() { update_ground_games(); } +void attempt_hourly_boar_spawn() { attempt_hourly_ground_game_spawn(); } +bool damage_boar_at(int pos, int damage) { return damage_ground_game_at(pos, damage); } diff --git a/src/world_state.nvgt b/src/world_state.nvgt index 38aefe2..722c473 100644 --- a/src/world_state.nvgt +++ b/src/world_state.nvgt @@ -7,93 +7,7 @@ int barricade_health = 0; bool barricade_initialized = false; int residents_count = 0; -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 Bandit { - int position; - int health; - string alert_sound; - string weapon_type; // "spear" or "axe" - int sound_handle; - timer move_timer; - timer alert_timer; - timer attack_timer; - int next_alert_delay; - int move_interval; - - // Wandering behavior properties - string behavior_state; // "aggressive" or "wandering" - int wander_direction; // -1, 0, or 1 - timer wander_direction_timer; - int wander_direction_change_interval; - - Bandit(int pos, int expansion_start, int expansion_end) { - // Spawn somewhere in the expanded area - position = random(expansion_start, expansion_end); - health = BANDIT_HEALTH; - sound_handle = -1; - - // Choose random alert sound - int sound_index = random(0, bandit_sounds.length() - 1); - alert_sound = bandit_sounds[sound_index]; - - // Choose random weapon - weapon_type = (random(0, 1) == 0) ? "spear" : "axe"; - - // Random movement speed within range - move_interval = random(BANDIT_MOVE_INTERVAL_MIN, BANDIT_MOVE_INTERVAL_MAX); - - move_timer.restart(); - alert_timer.restart(); - attack_timer.restart(); - next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY); - - // Initialize wandering behavior (start aggressive during invasion) - behavior_state = "aggressive"; - wander_direction = 0; - wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); - wander_direction_timer.restart(); - } -} -Bandit@[] bandits; - -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; @@ -788,422 +702,8 @@ WorldHerbGarden@ get_herb_garden_at_base() { return null; } -// Bandit Functions -void clear_bandits() { - if (bandits.length() == 0) return; - - for (uint i = 0; i < bandits.length(); i++) { - if (bandits[i].sound_handle != -1) { - p.destroy_sound(bandits[i].sound_handle); - bandits[i].sound_handle = -1; - } - } - bandits.resize(0); -} - -Bandit@ get_bandit_at(int pos) { - for (uint i = 0; i < bandits.length(); i++) { - if (bandits[i].position == pos) { - return @bandits[i]; - } - } - 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() { - for (uint i = 0; i < boars.length(); i++) { - update_boar(boars[i]); - } -} - -void attempt_hourly_boar_spawn() { - if (expanded_area_start == -1) return; - if (boars.length() >= BOAR_MAX_COUNT) return; - - if (random(1, 100) <= BOAR_SPAWN_CHANCE_PER_HOUR) { - spawn_boar(expanded_area_start, expanded_area_end); - } -} - -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++) { - int candidate = random(expansion_start, expansion_end); - if (get_bandit_at(candidate) == null) { - spawn_x = candidate; - break; - } - } - if (spawn_x == -1) { - spawn_x = random(expansion_start, expansion_end); - } - - Bandit@ b = Bandit(spawn_x, expansion_start, expansion_end); - bandits.insert_last(b); - b.sound_handle = play_creature_voice(b.alert_sound, x, spawn_x, BANDIT_SOUND_VOLUME_STEP); -} - -bool can_bandit_attack_player(Bandit@ bandit) { - if (player_health <= 0) { - return false; - } - - // Cannot attack player if barricade is up and player is in base - if (barricade_health > 0 && x <= BASE_END) { - return false; - } - - if (abs(bandit.position - x) > 1) { - return false; - } - - return y <= BANDIT_ATTACK_MAX_HEIGHT; -} - -bool try_attack_player_bandit(Bandit@ bandit) { - if (!can_bandit_attack_player(bandit)) { - return false; - } - if (bandit.attack_timer.elapsed < BANDIT_ATTACK_INTERVAL) { - return false; - } - - bandit.attack_timer.restart(); - - // Play weapon swing sound based on bandit's weapon - if (bandit.weapon_type == "spear") { - play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); - } else if (bandit.weapon_type == "axe") { - 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); - player_health -= damage; - if (player_health < 0) { - player_health = 0; - } - - // Play hit sound - if (bandit.weapon_type == "spear") { - play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); - } else if (bandit.weapon_type == "axe") { - play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); - } - - return true; -} - -void try_attack_barricade_bandit(Bandit@ bandit) { - if (barricade_health <= 0) return; - if (bandit.attack_timer.elapsed < BANDIT_ATTACK_INTERVAL) return; - - bandit.attack_timer.restart(); - - // Bandits do 1-2 damage to barricade - int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX); - barricade_health -= damage; - if (barricade_health < 0) barricade_health = 0; - - // Play weapon swing sound - if (bandit.weapon_type == "spear") { - 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") { - 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 - if (can_residents_defend()) { - int counterDamage = perform_resident_defense(); - if (counterDamage > 0) { - bandit.health -= counterDamage; - if (bandit.health <= 0 && x <= BASE_END) { - speak_with_history("Residents killed an attacking bandit.", true); - } - } - } - - if (barricade_health == 0) { - notify("The barricade has fallen!"); - } -} - -void update_bandit(Bandit@ bandit) { - // Play alert sound at intervals - if (bandit.alert_timer.elapsed > bandit.next_alert_delay) { - bandit.alert_timer.restart(); - bandit.next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY); - bandit.sound_handle = play_creature_voice(bandit.alert_sound, x, bandit.position, BANDIT_SOUND_VOLUME_STEP); - } - - if (try_attack_player_bandit(bandit)) { - return; - } - - if (bandit.move_timer.elapsed < bandit.move_interval) return; - bandit.move_timer.restart(); - - // If barricade is up and bandit is at the edge of base, attack barricade - if (barricade_health > 0 && bandit.position == BASE_END + 1) { - try_attack_barricade_bandit(bandit); - return; - } - - // State-based behavior - if (bandit.behavior_state == "wandering") { - // Check if player is within detection radius - int distance = abs(bandit.position - x); - if (distance <= BANDIT_DETECTION_RADIUS) { - // Player detected! Switch to aggressive - bandit.behavior_state = "aggressive"; - } else { - // Continue wandering - if (bandit.wander_direction_timer.elapsed > bandit.wander_direction_change_interval) { - // Time to change direction - bandit.wander_direction = random(-1, 1); - bandit.wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); - bandit.wander_direction_timer.restart(); - } - - // Move in wander direction (if not 0) - if (bandit.wander_direction != 0) { - int target_x = bandit.position + bandit.wander_direction; - - // Check bounds - if (target_x >= 0 && target_x < MAP_SIZE) { - // Don't wander into base if barricade is up - if (target_x <= BASE_END && barricade_health > 0) { - // Change direction instead - bandit.wander_direction = -bandit.wander_direction; - } else { - bandit.position = target_x; - play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); - } - } else { - // Hit map boundary, reverse direction - bandit.wander_direction = -bandit.wander_direction; - } - } - return; - } - } - - // Aggressive behavior (original logic) - if (bandit.behavior_state == "aggressive") { - // Move toward player - int direction = 0; - if (x > BASE_END) { - // Player is outside base, move toward them - if (x > bandit.position) { - direction = 1; - } else if (x < bandit.position) { - direction = -1; - } else { - return; - } - } else { - // Player is in base, move toward base edge - if (bandit.position > BASE_END + 1) { - direction = -1; - } else { - return; // Already at base edge - } - } - - int target_x = bandit.position + direction; - if (target_x < 0 || target_x >= MAP_SIZE) return; - - // Don't enter base if barricade is up - if (target_x <= BASE_END && barricade_health > 0) { - try_attack_barricade_bandit(bandit); - return; - } - - bandit.position = target_x; - play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); - } -} - -void update_bandits() { - for (uint i = 0; i < bandits.length(); i++) { - update_bandit(bandits[i]); - } -} - -bool damage_bandit_at(int pos, int damage) { - for (uint i = 0; i < bandits.length(); i++) { - if (bandits[i].position == pos) { - bandits[i].health -= damage; - if (bandits[i].health <= 0) { - if (bandits[i].sound_handle != -1) { - p.destroy_sound(bandits[i].sound_handle); - bandits[i].sound_handle = -1; - } - play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, BANDIT_SOUND_VOLUME_STEP); - bandits.remove_at(i); - } - return true; - } - } - return false; -} +// Bandit functions moved to src/enemies/bandit.nvgt +// Boar/GroundGame functions moved to src/enemies/ground_game.nvgt // Stream Functions void add_world_stream(int start_pos, int width) {