diff --git a/draugnorak.nvgt b/draugnorak.nvgt index c33f4a1..1cef639 100644 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -6,6 +6,10 @@ sound_pool p(100); #include "src/constants.nvgt" #include "src/player.nvgt" +#include "src/creature_base.nvgt" +#include "src/creature_death.nvgt" +#include "src/world_object_base.nvgt" +#include "src/enemies/undead.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 62f3bd6..e191345 100644 --- a/src/combat.nvgt +++ b/src/combat.nvgt @@ -166,9 +166,9 @@ void release_sling_attack(int player_x) { break; } - // Then check for zombie - Zombie@ zombie = get_zombie_at(check_x); - if (zombie != null) { + // Then check for undead + Undead@ undead = get_zombie_at(check_x); + if (undead != null) { target_x = check_x; hit_bandit = false; break; diff --git a/src/creature_base.nvgt b/src/creature_base.nvgt new file mode 100644 index 0000000..1b74adb --- /dev/null +++ b/src/creature_base.nvgt @@ -0,0 +1,18 @@ +// Base class for ground creatures (enemies and game animals) +// Provides common fields and methods for creatures that move on the ground + +class CreatureBase { + int position; + int health; + int sound_handle = -1; + timer move_timer; + timer attack_timer; + + // Common sound cleanup method + void destroy_sound() { + if (sound_handle != -1) { + p.destroy_sound(sound_handle); + sound_handle = -1; + } + } +} diff --git a/src/creature_death.nvgt b/src/creature_death.nvgt new file mode 100644 index 0000000..01b8ac0 --- /dev/null +++ b/src/creature_death.nvgt @@ -0,0 +1,21 @@ +// Unified death handling functions for creatures +// Ensures consistent death behavior: stop sound -> play death sound -> create drop + +// Handle creature death with sound cleanup and optional drop +// This consolidates the death pattern used across zombies, bandits, boars, etc. +void handle_creature_death(int creature_pos, string death_sound, float volume_step, string drop_type = "") { + // Play death sound at creature position + play_creature_death_sound(death_sound, x, creature_pos, volume_step); + + // Create world drop if specified + if (drop_type != "") { + add_world_drop(creature_pos, drop_type); + } +} + +// Cleanup sound handle before death (should be called before handle_creature_death) +void cleanup_creature_sound(int sound_handle_ref) { + if (sound_handle_ref != -1) { + p.destroy_sound(sound_handle_ref); + } +} diff --git a/src/enemies/undead.nvgt b/src/enemies/undead.nvgt new file mode 100644 index 0000000..4907392 --- /dev/null +++ b/src/enemies/undead.nvgt @@ -0,0 +1,217 @@ +// 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; + timer move_timer; + timer groan_timer; + timer attack_timer; + int next_groan_delay; + + 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; + move_timer.restart(); + groan_timer.restart(); + attack_timer.restart(); + next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY); + } +} +Undead@[] undeads; + +void clear_undeads() { + if (undeads.length() == 0) return; + + for (uint i = 0; i < undeads.length(); i++) { + 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); + z.sound_handle = play_creature_voice(z.voice_sound, x, spawn_x, 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/enemies/zombie_hits_player.ogg", x, undead.position, ZOMBIE_SOUND_VOLUME_STEP); + + // Resident defense counter-attack + if (can_residents_defend()) { + int counterDamage = perform_resident_defense(); + if (counterDamage > 0) { + undead.health -= counterDamage; + if (undead.health <= 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 (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_creature_attack_sound("sounds/enemies/zombie_hits_player.ogg", x, undead.position, ZOMBIE_SOUND_VOLUME_STEP); + return true; +} + +void update_undead(Undead@ undead) { + if (undead.groan_timer.elapsed > undead.next_groan_delay) { + undead.groan_timer.restart(); + undead.next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY); + undead.sound_handle = play_creature_voice(undead.voice_sound, x, undead.position, 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; + play_creature_footstep(x, undead.position, BASE_END, GRASS_END, ZOMBIE_FOOTSTEP_MAX_DISTANCE, ZOMBIE_SOUND_VOLUME_STEP); +} + +void update_undeads() { + if (is_daytime) { + clear_undeads(); + return; + } + + while (undeads.length() < ZOMBIE_MAX_COUNT) { + spawn_undead(); + } + + for (uint i = 0; i < undeads.length(); i++) { + update_undead(undeads[i]); + } +} + +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 (undeads[i].sound_handle != -1) { + p.destroy_sound(undeads[i].sound_handle); + undeads[i].sound_handle = -1; + } + play_creature_death_sound("sounds/enemies/enemy_falls.ogg", 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(); } diff --git a/src/world_object_base.nvgt b/src/world_object_base.nvgt new file mode 100644 index 0000000..41ffa62 --- /dev/null +++ b/src/world_object_base.nvgt @@ -0,0 +1,10 @@ +// Base class for simple world objects (buildings and structures) +// Provides common position field for world objects that only need to track location + +class WorldLocationBase { + int position; + + WorldLocationBase(int pos) { + position = pos; + } +} diff --git a/src/world_state.nvgt b/src/world_state.nvgt index 2b19ea0..38aefe2 100644 --- a/src/world_state.nvgt +++ b/src/world_state.nvgt @@ -7,35 +7,10 @@ int barricade_health = 0; bool barricade_initialized = false; int residents_count = 0; -string[] zombie_sounds = {"sounds/enemies/zombie1.ogg"}; 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 Zombie { - int position; - int health; - string voice_sound; - int sound_handle; - timer move_timer; - timer groan_timer; - timer attack_timer; - int next_groan_delay; - - Zombie(int pos) { - position = pos; - health = ZOMBIE_HEALTH; - int sound_index = random(0, zombie_sounds.length() - 1); - voice_sound = zombie_sounds[sound_index]; - sound_handle = -1; - move_timer.restart(); - groan_timer.restart(); - attack_timer.restart(); - next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY); - } -} -Zombie@[] zombies; - class Bandit { int position; int health; @@ -792,184 +767,7 @@ int add_barricade_health(int amount) { return barricade_health - before; } -void clear_zombies() { - if (zombies.length() == 0) return; - - for (uint i = 0; i < zombies.length(); i++) { - if (zombies[i].sound_handle != -1) { - p.destroy_sound(zombies[i].sound_handle); - zombies[i].sound_handle = -1; - } - } - zombies.resize(0); -} - -Zombie@ get_zombie_at(int pos) { - for (uint i = 0; i < zombies.length(); i++) { - if (zombies[i].position == pos) { - return @zombies[i]; - } - } - return null; -} - -void spawn_zombie() { - int spawn_x = -1; - for (int attempts = 0; attempts < 20; attempts++) { - int candidate = random(BASE_END + 1, MAP_SIZE - 1); - if (get_zombie_at(candidate) == null) { - spawn_x = candidate; - break; - } - } - if (spawn_x == -1) { - spawn_x = random(BASE_END + 1, MAP_SIZE - 1); - } - - Zombie@ z = Zombie(spawn_x); - zombies.insert_last(z); - z.sound_handle = play_creature_voice(z.voice_sound, x, spawn_x, ZOMBIE_SOUND_VOLUME_STEP); -} - -void try_attack_barricade(Zombie@ zombie) { - if (barricade_health <= 0) return; - if (zombie.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) return; - - zombie.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/enemies/zombie_hits_player.ogg", x, zombie.position, ZOMBIE_SOUND_VOLUME_STEP); - - // Resident defense counter-attack - if (can_residents_defend()) { - int counterDamage = perform_resident_defense(); - if (counterDamage > 0) { - zombie.health -= counterDamage; - if (zombie.health <= 0 && x <= BASE_END) { - speak_with_history("Residents killed an attacking zombie.", true); - } - } - } - - if (barricade_health == 0) { - notify("The barricade has fallen!"); - } -} - -bool can_zombie_attack_player(Zombie@ zombie) { - if (player_health <= 0) { - return false; - } - - if (barricade_health > 0 && x <= BASE_END) { - return false; - } - - if (abs(zombie.position - x) > 1) { - return false; - } - - return y <= ZOMBIE_ATTACK_MAX_HEIGHT; -} - -bool try_attack_player(Zombie@ zombie) { - if (!can_zombie_attack_player(zombie)) { - return false; - } - if (zombie.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) { - return false; - } - - zombie.attack_timer.restart(); - int damage = random(ZOMBIE_DAMAGE_MIN, ZOMBIE_DAMAGE_MAX); - player_health -= damage; - if (player_health < 0) { - player_health = 0; - } - - play_creature_attack_sound("sounds/enemies/zombie_hits_player.ogg", x, zombie.position, ZOMBIE_SOUND_VOLUME_STEP); - return true; -} - -void update_zombie(Zombie@ zombie) { - if (zombie.groan_timer.elapsed > zombie.next_groan_delay) { - zombie.groan_timer.restart(); - zombie.next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY); - zombie.sound_handle = play_creature_voice(zombie.voice_sound, x, zombie.position, ZOMBIE_SOUND_VOLUME_STEP); - } - - if (try_attack_player(zombie)) { - return; - } - - if (zombie.move_timer.elapsed < ZOMBIE_MOVE_INTERVAL) return; - zombie.move_timer.restart(); - - if (barricade_health > 0 && zombie.position == BASE_END + 1) { - try_attack_barricade(zombie); - return; - } - - int direction = 0; - if (x > BASE_END) { - if (x > zombie.position) { - direction = 1; - } else if (x < zombie.position) { - direction = -1; - } else { - return; - } - } else { - direction = random(-1, 1); - if (direction == 0) return; - } - - int target_x = zombie.position + direction; - if (target_x < 0 || target_x >= MAP_SIZE) return; - - if (target_x <= BASE_END && barricade_health > 0) { - try_attack_barricade(zombie); - return; - } - - zombie.position = target_x; - play_creature_footstep(x, zombie.position, BASE_END, GRASS_END, ZOMBIE_FOOTSTEP_MAX_DISTANCE, ZOMBIE_SOUND_VOLUME_STEP); -} - -void update_zombies() { - if (is_daytime) { - clear_zombies(); - return; - } - - while (zombies.length() < ZOMBIE_MAX_COUNT) { - spawn_zombie(); - } - - for (uint i = 0; i < zombies.length(); i++) { - update_zombie(zombies[i]); - } -} - -bool damage_zombie_at(int pos, int damage) { - for (uint i = 0; i < zombies.length(); i++) { - if (zombies[i].position == pos) { - zombies[i].health -= damage; - if (zombies[i].health <= 0) { - if (zombies[i].sound_handle != -1) { - p.destroy_sound(zombies[i].sound_handle); - zombies[i].sound_handle = -1; - } - play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, ZOMBIE_SOUND_VOLUME_STEP); - zombies.remove_at(i); - } - return true; - } - } - return false; -} +// Zombie functions moved to src/enemies/undead.nvgt WorldHerbGarden@ get_herb_garden_at(int pos) { for (uint i = 0; i < world_herb_gardens.length(); i++) {