// 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(); }