367 lines
12 KiB
Plaintext
367 lines
12 KiB
Plaintext
// 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;
|
|
}
|