use crate::{Direction, Entity, EntityMoveSpeed, Pos, Tile, astar}; pub const MIN_ROAM_DIST: u16 = 1; pub const MAX_ROAM_DIST: u16 = 4; pub const ZOMBIE_HEALTH: u32 = 50; pub const ENEMY_VISION_RADIUS: u16 = 5; pub const IDLE_WAIT: f32 = 1.; #[derive(Clone, Debug, PartialEq)] pub enum EnemyMoveState { Idle(f32), Roam(Vec), Attack(Vec), } impl EnemyMoveState { pub fn new_idle() -> Self { Self::Idle(0.) } pub fn new_attack(starting_pos: Pos, player_pos: Pos, tiles: &[Tile]) -> Option { if player_pos.manhattan(starting_pos) < ENEMY_VISION_RADIUS { let data = astar::find_path(tiles, starting_pos, player_pos)?; let mut path = data.0; path.reverse(); return Some(Self::Attack(path)); } None } pub fn new_roam(starting_pos: Pos, tiles: &[Tile]) -> Self { let mut rand_dir = Direction::get_random_dir(); let mut rand_tiles = rand::random_range(MIN_ROAM_DIST..=MAX_ROAM_DIST); let mut loop_index = 0; loop { if let Some(p) = starting_pos.step_by(rand_dir, rand_tiles) { if !tiles[p.idx()].is_wall() { if let Some(data) = astar::find_path(tiles, starting_pos, p) { let mut path = data.0; path.reverse(); return Self::Roam(path); } } } rand_dir = Direction::get_random_dir(); rand_tiles = rand::random_range(MIN_ROAM_DIST..=MAX_ROAM_DIST); // Safety check loop_index += 1; assert!( loop_index < 100, "EnemyMoveState::new_roam couldn't find a valid spot around {starting_pos:?} in between {MIN_ROAM_DIST}-{MAX_ROAM_DIST} tiles", ); } } } #[derive(Clone, Debug, PartialEq, Eq)] pub enum EnemyAttackType { Melee, Ranged, } #[derive(Clone, Debug, PartialEq, Eq, Copy)] pub enum EnemyType { Zombie, } #[derive(Clone, Debug, PartialEq)] pub struct Enemy { pub entity: Entity, pub enemy_type: EnemyType, pub attack_type: EnemyAttackType, move_state: EnemyMoveState, } impl Enemy { pub fn new(enemy_type: EnemyType, pos: Pos) -> Self { match enemy_type { EnemyType::Zombie => Self::zombie(pos), } } fn _new( pos: Pos, health: u32, move_speed: EntityMoveSpeed, enemy_type: EnemyType, attack_type: EnemyAttackType, ) -> Self { let entity = Entity::enemy(pos, move_speed, health); Self { entity, enemy_type, attack_type, move_state: EnemyMoveState::new_idle(), } } fn zombie(pos: Pos) -> Self { Self::_new( pos, ZOMBIE_HEALTH, EntityMoveSpeed::Slow, EnemyType::Zombie, EnemyAttackType::Melee, ) } pub fn handle_movement(&mut self, player_pos: Pos, delta_time: f32, tiles: &[Tile]) { // Check if player is in range if !matches!(self.move_state, EnemyMoveState::Attack { .. }) && let Some(move_state) = EnemyMoveState::new_attack(self.entity.pos, player_pos, tiles) { self.move_state = move_state; return; } match &mut self.move_state { EnemyMoveState::Idle(idle_accum) => { *idle_accum += delta_time; if *idle_accum < IDLE_WAIT { return; } self.move_state = EnemyMoveState::new_roam(self.entity.pos, tiles); } EnemyMoveState::Roam(moves) => { let p = if let Some(p) = moves.last() { p } else { self.move_state = EnemyMoveState::new_idle(); return; }; Self::movement_helper(&mut self.entity, *p, moves, delta_time); } EnemyMoveState::Attack(moves) => { // Stop at one move left so the enemy is flush with the player tile if moves.len() == 1 { return; } Self::movement_helper( &mut self.entity, *moves.last().unwrap_or(&Pos::default()), moves, delta_time, ); } } } fn movement_helper( entity: &mut Entity, next_move: Pos, moves: &mut Vec, delta_time: f32, ) { if entity.pos.y() > next_move.y() { entity.move_by_dir(Direction::North, delta_time); if entity.fpos.y() <= next_move.y() as f32 { if let Some(p) = moves.pop() { entity.pos = p; } } } else if entity.pos.y() < next_move.y() { entity.move_by_dir(Direction::South, delta_time); if entity.fpos.y() >= next_move.y() as f32 { if let Some(p) = moves.pop() { entity.pos = p; } } } else if entity.pos.x() < next_move.x() { entity.move_by_dir(Direction::East, delta_time); if entity.fpos.x() >= next_move.x() as f32 { if let Some(p) = moves.pop() { entity.pos = p; } } } else { entity.move_by_dir(Direction::West, delta_time); if entity.fpos.x() <= next_move.x() as f32 { if let Some(p) = moves.pop() { entity.pos = p; } } }; } }