// Bandit enemies // Hostile humans that spawn during invasions and wander in expanded areas string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"}; string[] get_invader_sound_list(const string&in invader_type) { string[] sounds; if (invader_type == "") { return sounds; } for (int i = 1; i <= INVADER_SOUND_VARIANTS_MAX; i++) { string sound_file = "sounds/enemies/" + invader_type + i + ".ogg"; if (file_exists(sound_file)) { sounds.insert_last(sound_file); } } return sounds; } string pick_invader_alert_sound(const string&in invader_type) { string[] sounds = get_invader_sound_list(invader_type); if (sounds.length() == 0) { sounds = bandit_sounds; } if (sounds.length() == 0) { return ""; } int sound_index = random(0, sounds.length() - 1); return sounds[sound_index]; } class Bandit { int position; int health; string alert_sound; string invader_type; string weapon_type; // "spear" or "axe" int sound_handle; bool in_weapon_range; timer move_timer; timer attack_timer; 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, string invader = "bandit") { // Spawn somewhere in the expanded area position = random(expansion_start, expansion_end); health = BANDIT_HEALTH; sound_handle = -1; invader_type = invader; // Choose random alert sound alert_sound = pick_invader_alert_sound(invader_type); // 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(); attack_timer.restart(); // 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(); in_weapon_range = false; } } Bandit@[] bandits; void update_bandit_weapon_range_audio() { for (uint i = 0; i < bandits.length(); i++) { update_weapon_range_audio(bandits[i].position, bandits[i].in_weapon_range); } } bool bandit_range_audio_registered = false; void ensure_bandit_range_audio_registration() { if (bandit_range_audio_registered) return; bandit_range_audio_registered = register_weapon_range_audio_callback(@update_bandit_weapon_range_audio); } void clear_bandits() { if (bandits.length() == 0) return; for (uint i = 0; i < bandits.length(); i++) { force_weapon_range_exit(bandits[i].position, bandits[i].in_weapon_range); 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, const string&in invader_type = "bandit") { 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, invader_type); bandits.insert_last(b); // Play looping sound that follows the bandit int[] areaStarts; int[] areaEnds; get_active_audio_areas(areaStarts, areaEnds); if (areaStarts.length() == 0 || range_overlaps_active_areas(spawn_x, spawn_x, areaStarts, areaEnds)) { b.sound_handle = play_1d_with_volume_step(b.alert_sound, x, spawn_x, true, 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); } play_player_damage_sound(); 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 (barricade hits share a common impact sound) 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); } 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) { int before_health = bandit.health; damage_bandit_at(bandit.position, counterDamage); if (before_health - counterDamage <= 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, bool audio_active) { // Update looping sound position if (!audio_active) { if (bandit.sound_handle != -1) { p.destroy_sound(bandit.sound_handle); bandit.sound_handle = -1; } } else if (bandit.sound_handle != -1 && p.sound_is_active(bandit.sound_handle)) { p.update_sound_1d(bandit.sound_handle, bandit.position); } else if (bandit.sound_handle == -1 || !p.sound_is_active(bandit.sound_handle)) { // Restart looping sound if it stopped if (bandit.sound_handle != -1) { p.destroy_sound(bandit.sound_handle); } bandit.sound_handle = play_1d_with_volume_step(bandit.alert_sound, x, bandit.position, true, 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; if (audio_active) { 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; if (audio_active) { play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); } } } void update_bandits() { ensure_bandit_range_audio_registration(); int[] areaStarts; int[] areaEnds; get_active_audio_areas(areaStarts, areaEnds); bool limit_audio = (areaStarts.length() > 0); for (uint i = 0; i < bandits.length(); i++) { bool audio_active = !limit_audio || range_overlaps_active_areas(bandits[i].position, bandits[i].position, areaStarts, areaEnds); update_bandit(bandits[i], audio_active); } } 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_sounds("sounds/enemies/enemy_falls.ogg", bandits[i].alert_sound, x, pos, BANDIT_SOUND_VOLUME_STEP); bandits.remove_at(i); } return true; } } return false; }