269 lines
8.0 KiB
Plaintext
269 lines
8.0 KiB
Plaintext
// Undead creatures (zombies, vampires, ghosts, etc.)
|
|
// Currently only zombies are implemented
|
|
|
|
string[] undead_zombie_sounds = {"sounds/enemies/zombie1.ogg"};
|
|
|
|
class Undead {
|
|
int position;
|
|
int health;
|
|
string undead_type; // "zombie", future: "vampire", "ghost", etc.
|
|
string voice_sound;
|
|
int sound_handle;
|
|
bool in_weapon_range;
|
|
timer move_timer;
|
|
timer attack_timer;
|
|
|
|
Undead(int pos, string type = "zombie") {
|
|
position = pos;
|
|
undead_type = type;
|
|
health = ZOMBIE_HEALTH;
|
|
int sound_index = random(0, undead_zombie_sounds.length() - 1);
|
|
voice_sound = undead_zombie_sounds[sound_index];
|
|
sound_handle = -1;
|
|
in_weapon_range = false;
|
|
move_timer.restart();
|
|
attack_timer.restart();
|
|
}
|
|
}
|
|
Undead@[] undeads;
|
|
|
|
void update_undead_weapon_range_audio() {
|
|
for (uint i = 0; i < undeads.length(); i++) {
|
|
update_weapon_range_audio(undeads[i].position, undeads[i].in_weapon_range);
|
|
}
|
|
}
|
|
bool undead_range_audio_registered = false;
|
|
|
|
void ensure_undead_range_audio_registration() {
|
|
if (undead_range_audio_registered) return;
|
|
undead_range_audio_registered = register_weapon_range_audio_callback(@update_undead_weapon_range_audio);
|
|
}
|
|
|
|
void clear_undeads() {
|
|
if (undeads.length() == 0) return;
|
|
|
|
for (uint i = 0; i < undeads.length(); i++) {
|
|
force_weapon_range_exit(undeads[i].position, undeads[i].in_weapon_range);
|
|
if (undeads[i].sound_handle != -1) {
|
|
p.destroy_sound(undeads[i].sound_handle);
|
|
undeads[i].sound_handle = -1;
|
|
}
|
|
}
|
|
undeads.resize(0);
|
|
}
|
|
|
|
Undead@ get_undead_at(int pos) {
|
|
for (uint i = 0; i < undeads.length(); i++) {
|
|
if (undeads[i].position == pos) {
|
|
return @undeads[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void spawn_undead() {
|
|
int spawn_x = -1;
|
|
for (int attempts = 0; attempts < 20; attempts++) {
|
|
int candidate = random(BASE_END + 1, MAP_SIZE - 1);
|
|
if (get_undead_at(candidate) == null) {
|
|
spawn_x = candidate;
|
|
break;
|
|
}
|
|
}
|
|
if (spawn_x == -1) {
|
|
spawn_x = random(BASE_END + 1, MAP_SIZE - 1);
|
|
}
|
|
|
|
Undead@ z = Undead(spawn_x, "zombie");
|
|
undeads.insert_last(z);
|
|
// Play looping sound that follows the zombie
|
|
int[] areaStarts;
|
|
int[] areaEnds;
|
|
get_active_audio_areas(areaStarts, areaEnds);
|
|
if (areaStarts.length() == 0 || range_overlaps_active_areas(spawn_x, spawn_x, areaStarts, areaEnds)) {
|
|
z.sound_handle = play_1d_with_volume_step(z.voice_sound, x, spawn_x, true, ZOMBIE_SOUND_VOLUME_STEP);
|
|
}
|
|
}
|
|
|
|
void try_attack_barricade_undead(Undead@ undead) {
|
|
if (barricade_health <= 0) return;
|
|
if (undead.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) return;
|
|
|
|
undead.attack_timer.restart();
|
|
int damage = random(ZOMBIE_DAMAGE_MIN, ZOMBIE_DAMAGE_MAX);
|
|
barricade_health -= damage;
|
|
if (barricade_health < 0) barricade_health = 0;
|
|
|
|
play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, undead.position, ZOMBIE_SOUND_VOLUME_STEP);
|
|
|
|
// Resident defense counter-attack
|
|
if (can_residents_defend()) {
|
|
int counterDamage = perform_resident_defense();
|
|
if (counterDamage > 0) {
|
|
int before_health = undead.health;
|
|
damage_undead_at(undead.position, counterDamage);
|
|
if (before_health - counterDamage <= 0 && x <= BASE_END) {
|
|
speak_with_history("Residents killed an attacking zombie.", true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (barricade_health == 0) {
|
|
notify("The barricade has fallen!");
|
|
}
|
|
}
|
|
|
|
bool can_undead_attack_player(Undead@ undead) {
|
|
if (player_health <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if (barricade_health > 0 && x <= BASE_END) {
|
|
return false;
|
|
}
|
|
|
|
if (fylgjaCharging) {
|
|
return false;
|
|
}
|
|
|
|
if (abs(undead.position - x) > 1) {
|
|
return false;
|
|
}
|
|
|
|
return y <= ZOMBIE_ATTACK_MAX_HEIGHT;
|
|
}
|
|
|
|
bool try_attack_player_undead(Undead@ undead) {
|
|
if (!can_undead_attack_player(undead)) {
|
|
return false;
|
|
}
|
|
if (undead.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) {
|
|
return false;
|
|
}
|
|
|
|
undead.attack_timer.restart();
|
|
int damage = random(ZOMBIE_DAMAGE_MIN, ZOMBIE_DAMAGE_MAX);
|
|
player_health -= damage;
|
|
if (player_health < 0) {
|
|
player_health = 0;
|
|
}
|
|
|
|
play_player_damage_sound();
|
|
return true;
|
|
}
|
|
|
|
void update_undead(Undead@ undead, bool audio_active) {
|
|
// Update looping sound position
|
|
if (!audio_active) {
|
|
if (undead.sound_handle != -1) {
|
|
p.destroy_sound(undead.sound_handle);
|
|
undead.sound_handle = -1;
|
|
}
|
|
} else if (undead.sound_handle != -1 && p.sound_is_active(undead.sound_handle)) {
|
|
p.update_sound_1d(undead.sound_handle, undead.position);
|
|
} else if (undead.sound_handle == -1 || !p.sound_is_active(undead.sound_handle)) {
|
|
// Restart looping sound if it stopped for some reason
|
|
if (undead.sound_handle != -1) {
|
|
p.destroy_sound(undead.sound_handle);
|
|
}
|
|
undead.sound_handle = play_1d_with_volume_step(undead.voice_sound, x, undead.position, true, ZOMBIE_SOUND_VOLUME_STEP);
|
|
}
|
|
|
|
if (try_attack_player_undead(undead)) {
|
|
return;
|
|
}
|
|
|
|
if (undead.move_timer.elapsed < ZOMBIE_MOVE_INTERVAL) return;
|
|
undead.move_timer.restart();
|
|
|
|
if (barricade_health > 0 && undead.position == BASE_END + 1) {
|
|
try_attack_barricade_undead(undead);
|
|
return;
|
|
}
|
|
|
|
int direction = 0;
|
|
if (x > BASE_END) {
|
|
if (x > undead.position) {
|
|
direction = 1;
|
|
} else if (x < undead.position) {
|
|
direction = -1;
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
direction = random(-1, 1);
|
|
if (direction == 0) return;
|
|
}
|
|
|
|
int target_x = undead.position + direction;
|
|
if (target_x < 0 || target_x >= MAP_SIZE) return;
|
|
|
|
if (target_x <= BASE_END && barricade_health > 0) {
|
|
try_attack_barricade_undead(undead);
|
|
return;
|
|
}
|
|
|
|
undead.position = target_x;
|
|
if (audio_active) {
|
|
play_creature_footstep(x, undead.position, BASE_END, GRASS_END, ZOMBIE_FOOTSTEP_MAX_DISTANCE, ZOMBIE_SOUND_VOLUME_STEP);
|
|
}
|
|
}
|
|
|
|
void update_undeads() {
|
|
ensure_undead_range_audio_registration();
|
|
if (is_daytime) {
|
|
clear_undeads();
|
|
return;
|
|
}
|
|
|
|
int extra = 0;
|
|
if (MAP_SIZE > 35) {
|
|
extra = (MAP_SIZE - 35) / 15;
|
|
}
|
|
int maxCount = ZOMBIE_MAX_COUNT + extra;
|
|
if (maxCount > ZOMBIE_MAX_COUNT_CAP) maxCount = ZOMBIE_MAX_COUNT_CAP;
|
|
|
|
while (undeads.length() < maxCount) {
|
|
spawn_undead();
|
|
}
|
|
|
|
int[] areaStarts;
|
|
int[] areaEnds;
|
|
get_active_audio_areas(areaStarts, areaEnds);
|
|
bool limit_audio = (areaStarts.length() > 0);
|
|
|
|
for (uint i = 0; i < undeads.length(); i++) {
|
|
bool audio_active = !limit_audio || range_overlaps_active_areas(undeads[i].position, undeads[i].position, areaStarts, areaEnds);
|
|
update_undead(undeads[i], audio_active);
|
|
}
|
|
}
|
|
|
|
bool damage_undead_at(int pos, int damage) {
|
|
for (uint i = 0; i < undeads.length(); i++) {
|
|
if (undeads[i].position == pos) {
|
|
undeads[i].health -= damage;
|
|
if (undeads[i].health <= 0) {
|
|
if (world_altars.length() > 0) {
|
|
favor += 0.2;
|
|
}
|
|
if (undeads[i].sound_handle != -1) {
|
|
p.destroy_sound(undeads[i].sound_handle);
|
|
undeads[i].sound_handle = -1;
|
|
}
|
|
play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", undeads[i].voice_sound, x, pos, ZOMBIE_SOUND_VOLUME_STEP);
|
|
undeads.remove_at(i);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Backward compatibility aliases (to be removed after full refactoring)
|
|
Undead@[]@ zombies = @undeads; // Array alias for backward compatibility
|
|
Undead@ get_zombie_at(int pos) { return get_undead_at(pos); }
|
|
bool damage_zombie_at(int pos, int damage) { return damage_undead_at(pos, damage); }
|
|
void update_zombies() { update_undeads(); }
|
|
void clear_zombies() { clear_undeads(); }
|
|
void spawn_zombie() { spawn_undead(); }
|