diff options
| author | Ryan Symons <47405201+rsymons22@users.noreply.github.com> | 2025-11-07 20:13:51 -0500 |
|---|---|---|
| committer | Ryan Symons <47405201+rsymons22@users.noreply.github.com> | 2025-11-07 20:13:51 -0500 |
| commit | 2ed3362b566079faa825b0cd6e5435b92f085df2 (patch) | |
| tree | e0b6521790bdd9ffa0ebb9bba0056bcb08c41184 | |
| parent | remove unused assets (diff) | |
| download | DungeonCrawl-2ed3362b566079faa825b0cd6e5435b92f085df2.tar.gz DungeonCrawl-2ed3362b566079faa825b0cd6e5435b92f085df2.tar.bz2 DungeonCrawl-2ed3362b566079faa825b0cd6e5435b92f085df2.zip | |
Moved Enemy logic to Entity Struct, fixed enemy drift while moving
| -rw-r--r-- | dungeon/src/enemy.rs | 194 | ||||
| -rw-r--r-- | dungeon/src/entity.rs | 219 | ||||
| -rw-r--r-- | dungeon/src/lib.rs | 7 | ||||
| -rw-r--r-- | game/src/main.rs | 2 | ||||
| -rw-r--r-- | graphics/src/render.rs | 4 |
5 files changed, 189 insertions, 237 deletions
diff --git a/dungeon/src/enemy.rs b/dungeon/src/enemy.rs deleted file mode 100644 index 0d432dd..0000000 --- a/dungeon/src/enemy.rs +++ /dev/null @@ -1,194 +0,0 @@ -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<Pos>), - Attack(Vec<Pos>), -} - -impl EnemyMoveState { - pub fn new_idle() -> Self { - Self::Idle(0.) - } - - pub fn new_attack(starting_pos: Pos, player_pos: Pos, tiles: &[Tile]) -> Option<Self> { - 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<Pos>, - 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; - } - } - }; - } -} diff --git a/dungeon/src/entity.rs b/dungeon/src/entity.rs index f20c534..aa77d98 100644 --- a/dungeon/src/entity.rs +++ b/dungeon/src/entity.rs @@ -1,6 +1,6 @@ //! The `entity` module contains structures of all entities including players and enimies. -use crate::{Direction, FPos, Pos, const_pos}; +use crate::{Direction, FPos, Pos, Tile, astar, const_pos}; /// `PLAYER_FULL_HEALTH` is the starting health of the player entity pub const PLAYER_FULL_HEALTH: u32 = 10; @@ -17,28 +17,79 @@ pub enum Item { } /// The `EntityKind` represents what kind of entity this is. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq)] pub enum EntityKind { /// The main player Player, - Enemy, + Zombie(EnemyMoveState), /// An item (not in an inventory) on the floor of the dungeon Item(Item), } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum EntityMoveSpeed { - Slow, - Medium, - Fast, -} -impl EntityMoveSpeed { - /// Returns value in tiles/second - pub fn value(&self) -> f32 { +impl EntityKind { + // Tiles/s + pub fn move_speed(&self) -> f32 { match &self { - Self::Slow => 1., - Self::Medium => 2., - Self::Fast => 3., + Self::Player => 2., + Self::Zombie(_) => 4., + _ => 0., + } + } +} + +pub const MIN_ROAM_DIST: u16 = 1; +pub const MAX_ROAM_DIST: u16 = 4; + +pub const ZOMBIE_HEALTH: u32 = 5; +pub const ENEMY_VISION_RADIUS: u16 = 10; + +pub const IDLE_WAIT: f32 = 1.; + +#[derive(Clone, Debug, PartialEq)] +pub enum EnemyMoveState { + Idle(f32), + Roam(Vec<Pos>), + Attack(Vec<Pos>), +} + +impl EnemyMoveState { + pub const fn new_idle() -> Self { + Self::Idle(0.) + } + + pub fn new_attack(starting_pos: Pos, player_pos: Pos, tiles: &[Tile]) -> Option<Self> { + 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; + if loop_index >= 100 { + return Self::new_idle(); + } } } } @@ -46,7 +97,7 @@ impl EntityMoveSpeed { /// The `Entity` kind represents the main player, or any other /// ai autonomous character that can move freely across the /// dungeon. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Entity { /// The fixed grid position of the entity pub pos: Pos, @@ -56,8 +107,6 @@ pub struct Entity { pub dir: Direction, /// Which kind this entity is (along with entity kind specific data) pub kind: EntityKind, - /// Move speed of this entity - pub move_speed: EntityMoveSpeed, /// The amount of health this entity has (None if this Entity does not use health) pub health: Option<u32>, } @@ -67,21 +116,19 @@ impl Entity { /// # Examples /// /// ``` - /// use dungeon::{Pos, Direction, Entity, EntityKind, EntityMoveSpeed}; + /// use dungeon::{Pos, Direction, Entity, EntityKind}; /// /// let pos = Pos::new(0, 0).unwrap(); /// let dir = Direction::North; /// let kind = EntityKind::Player; /// let health = Some(10); - /// let move_speed = EntityMoveSpeed::Medium; - /// let entity = Entity::new(pos, dir, kind, move_speed, health); + /// let entity = Entity::new(pos, dir, kind, health); /// ``` #[must_use] pub const fn new( pos: Pos, dir: Direction, kind: EntityKind, - move_speed: EntityMoveSpeed, health: Option<u32>, ) -> Self { let fpos = FPos::from_pos(pos); @@ -90,7 +137,6 @@ impl Entity { fpos, dir, kind, - move_speed, health, } } @@ -109,36 +155,139 @@ impl Entity { pub const fn player(pos: Pos) -> Self { let dir = Direction::East; let kind = EntityKind::Player; - let move_speed = EntityMoveSpeed::Medium; let health = Some(PLAYER_FULL_HEALTH); - Self::new(pos, dir, kind, move_speed, health) + Self::new(pos, dir, kind, health) } - /// Creates an Enemy version of the `Entity` + /// Creates the Zombie version of the `Entity` /// /// # Examples /// /// ``` - /// use dungeon::{Pos, Entity, EntityMoveSpeed}; + /// use dungeon::{Pos, Entity}; /// /// let pos = Pos::new(0, 0).unwrap(); - /// let move_speed = EntityMoveSpeed::Medium; - /// let health = 8; - /// let enemy = Entity::enemy(pos, move_speed, health); + /// let player = Entity::zombie(pos); /// ``` - #[must_use] - pub const fn enemy(pos: Pos, move_speed: EntityMoveSpeed, health: u32) -> Self { + pub const fn zombie(pos: Pos) -> Self { let dir = Direction::East; - let kind = EntityKind::Enemy; - Self::new(pos, dir, kind, move_speed, Some(health)) + let kind = EntityKind::Zombie(EnemyMoveState::new_idle()); + let health = Some(ZOMBIE_HEALTH); + Self::new(pos, dir, kind, health) } pub fn move_by_dir(&mut self, dir: Direction, delta_time: f32) { - if let Some(fp) = self.fpos.step_by(dir, delta_time * self.move_speed.value()) { + if let Some(fp) = self.fpos.step_by(dir, delta_time * self.kind.move_speed()) { // TODO: collision self.fpos = fp; } } + + pub fn handle_movement(&mut self, player_pos: Pos, tiles: &[Tile], delta_time: f32) { + match &self.kind { + EntityKind::Zombie(move_state) => { + self.zombie_movement(move_state.clone(), player_pos, delta_time, tiles); + } + EntityKind::Player => {} + _ => {} + } + } + + pub fn zombie_movement( + &mut self, + move_state: EnemyMoveState, + player_pos: Pos, + delta_time: f32, + tiles: &[Tile], + ) { + // Check if player is in range + if !matches!(move_state, EnemyMoveState::Attack { .. }) + && let Some(m_state) = EnemyMoveState::new_attack(self.pos, player_pos, tiles) + { + self.kind = EntityKind::Zombie(m_state); + return; + } + + match move_state { + EnemyMoveState::Idle(idle_accum) => { + if idle_accum < IDLE_WAIT { + self.kind = + EntityKind::Zombie(EnemyMoveState::Idle(idle_accum + delta_time)); + return; + } + + self.kind = EntityKind::Zombie(EnemyMoveState::new_roam(self.pos, tiles)); + } + EnemyMoveState::Roam(mut moves) => { + let p = if let Some(p) = moves.last() { + p + } else { + self.kind = EntityKind::Zombie(EnemyMoveState::new_idle()); + return; + }; + + Self::movement_helper(self, *p, &mut moves, delta_time); + + self.kind = EntityKind::Zombie(EnemyMoveState::Roam(moves)); + } + EnemyMoveState::Attack(mut moves) => { + // Stop at one move left so the enemy is flush with the player tile + if moves.len() == 1 { + return; + } + + Self::movement_helper( + self, + *moves.last().unwrap_or(&Pos::default()), + &mut moves, + delta_time, + ); + + self.kind = EntityKind::Zombie(EnemyMoveState::Attack(moves)); + } + } + } + + fn movement_helper( + entity: &mut Self, + next_move: Pos, + moves: &mut Vec<Pos>, + 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; + entity.fpos = FPos::from_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; + entity.fpos = FPos::from_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; + entity.fpos = FPos::from_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; + entity.fpos = FPos::from_pos(p); + } + } + }; + } } /// The `Player` type represents the main player entity diff --git a/dungeon/src/lib.rs b/dungeon/src/lib.rs index e85b718..cfd2fbe 100644 --- a/dungeon/src/lib.rs +++ b/dungeon/src/lib.rs @@ -3,7 +3,6 @@ pub mod astar; pub mod bsp; -pub mod enemy; pub mod entity; pub mod map; pub mod pos; @@ -13,15 +12,13 @@ pub use entity::*; pub use map::*; pub use pos::*; -use crate::enemy::{Enemy, EnemyType}; - /// The `Dungeon` type represents the game state of the /// dungeon crawler. #[derive(Clone, Debug, PartialEq)] pub struct Dungeon { pub floor: Floor, pub player: Player, - pub enemies: Vec<Enemy>, + pub enemies: Vec<Entity>, } impl Dungeon { /// Creates a new `Dungeon`. @@ -71,7 +68,7 @@ impl From<Floor> for Dungeon { // TODO: initalize rest of game state // TODO: Randomize enemy positions/types - let enemies = vec![Enemy::new(EnemyType::Zombie, floor.random_pos())]; + let enemies = vec![Entity::zombie(floor.random_pos())]; Self { floor, diff --git a/game/src/main.rs b/game/src/main.rs index 919e22c..1c76abc 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -14,8 +14,8 @@ fn main() -> Result<()> { for enemy in dungeon.enemies.iter_mut() { enemy.handle_movement( dungeon.player.entity.pos, + dungeon.floor.tiles(), window.delta_time(), - dungeon.floor.tiles_mut(), ); } diff --git a/graphics/src/render.rs b/graphics/src/render.rs index a6b9af0..1fe4b85 100644 --- a/graphics/src/render.rs +++ b/graphics/src/render.rs @@ -357,7 +357,7 @@ impl Renderer { { self.draw_entity(r, &dungeon.player.entity); for enemy in &dungeon.enemies { - self.draw_entity(r, &enemy.entity); + self.draw_entity(r, enemy); } } @@ -468,7 +468,7 @@ impl Renderer { // Draw enemy dots for enemy in &dungeon.enemies { - draw_dot(enemy.entity.pos, Color::RED); + draw_dot(enemy.pos, Color::RED); } // Draw player dot |