diff options
| author | Ryan Symons <47405201+rsymons22@users.noreply.github.com> | 2025-11-10 21:33:34 -0500 |
|---|---|---|
| committer | Ryan Symons <47405201+rsymons22@users.noreply.github.com> | 2025-11-10 21:33:34 -0500 |
| commit | 439395cca406c2d7ee17868c889222832a4cd686 (patch) | |
| tree | a9fb4ed7efe5df8d31574c5d874aca911ad13f4b | |
| parent | update checkpoint doc (diff) | |
| download | DungeonCrawl-439395cca406c2d7ee17868c889222832a4cd686.tar.gz DungeonCrawl-439395cca406c2d7ee17868c889222832a4cd686.tar.bz2 DungeonCrawl-439395cca406c2d7ee17868c889222832a4cd686.zip | |
Enemy now follows a moving player, updated checkpoint doc a bit
| -rw-r--r-- | docs/Checkpoint.md | 6 | ||||
| -rw-r--r-- | dungeon/src/entity.rs | 80 |
2 files changed, 58 insertions, 28 deletions
diff --git a/docs/Checkpoint.md b/docs/Checkpoint.md index ec891e0..9e77155 100644 --- a/docs/Checkpoint.md +++ b/docs/Checkpoint.md @@ -20,12 +20,11 @@ Give a summary of the progress made and lessons learned thus far. So far, our group has been able to achieve most of our MVP functionalities, including: - Dunegon floor procedural dungeon generation -- Player movement and enemy spawning +- Player movement and complex enemy movement (roaming and pathing to player). - Collision Logic We've even managed to achieve some stretch goals and additional features: - Usage of pixel art sprite sheets - Item/Inventory system -However, we still have yet to implement a full enemy/player combat loop (attacking and defending). ## Lessons Learned With the work completed so far, we've gained a greater understanding of creating Rust @@ -108,6 +107,9 @@ Implemented Features: - Finalize game textures - Many textures are placeholders, or straight up "error" textures +- Implement attacking for both enemies and player + +- Create new enemy types for a more interesting game experience, currently only a basic "Zombie" exists TODO ## Additional Details diff --git a/dungeon/src/entity.rs b/dungeon/src/entity.rs index 762355e..82042b5 100644 --- a/dungeon/src/entity.rs +++ b/dungeon/src/entity.rs @@ -13,6 +13,19 @@ pub const PLAYER_INVENTORY_SIZE: u16 = 5; /// `PLAYER_INVENTORY_SIZE_USIZE` is the maximum size of the inventory pub const PLAYER_INVENTORY_SIZE_USIZE: usize = PLAYER_INVENTORY_SIZE as usize; +/// 'MIN_ROAM_DIST' and 'MAX_ROAM_DIST' are the enemy roam ranges +pub const MIN_ROAM_DIST: u16 = 1; +pub const MAX_ROAM_DIST: u16 = 4; + +/// 'ZOMBIE_HEALTH' is the starting health of a zombie enemy +pub const ZOMBIE_HEALTH: u32 = 5; + +/// 'ENEMY_VISION_RADIUS' is the range used to determine if the player is seen by an enemy +pub const ENEMY_VISION_RADIUS: u16 = 10; + +/// 'IDLE_WAIT' is how long in seconds an enemy waits in the idle state +pub const IDLE_WAIT: f32 = 1.; + /// The `Item` type represents any item an entity may be using #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Item { @@ -32,7 +45,7 @@ pub enum EntityKind { } impl EntityKind { - // Tiles/s + /// Returns the move speed value for this type of entity in tiles/s pub fn move_speed(&self) -> f32 { match &self { Self::Player => 5., @@ -42,36 +55,41 @@ impl EntityKind { } } -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.; - +/// The `EnemyMoveState` enum describes the state of an enemies movement, either Idle +/// Roam, or Attack. Idle carries a `f32` value to remember time waited in idle. Both Roam +/// and Attack carry a `Vec<Pos>` to represent the sequence of moves to their desired locations. +/// Attack also holds the desired location itself as a `Pos`. #[derive(Clone, Debug, PartialEq)] pub enum EnemyMoveState { Idle(f32), Roam(Vec<Pos>), - Attack(Vec<Pos>), + Attack(Vec<Pos>, Pos), } impl EnemyMoveState { + /// Creates an `EnemyMoveState::Idle` enum, initialized to a time accumlation of 0. pub const fn new_idle() -> Self { Self::Idle(0.) } + /// Creates an `EnemyMoveState::Attack` enum containing the list of steps for movement and the goal position. + /// + /// Will return `Some(EnemyMoveState::Attack)` if the player is within the vision radius and there is a valid path + /// to the player. Returns `None` if the player is outside the radius or there is no valid path to the player. pub fn new_attack(starting_pos: Pos, player_pos: Pos, floor: &Floor) -> Option<Self> { if player_pos.manhattan(starting_pos) < ENEMY_VISION_RADIUS { let data = astar::astar(starting_pos, player_pos, floor)?; let mut path = data.0; path.reverse(); - return Some(Self::Attack(path)); + return Some(Self::Attack(path, player_pos)); } None } + /// Creates an `EnemyMoveState::Roam` enum containing the list of steps for movement. + /// + /// Will randomly pick a direction and number of tiles within roaming range to move. + /// If an invalid tile is selected 50 times in a row, `EnemyMoveState::Idle` is returned instead. pub fn new_roam(starting_pos: Pos, floor: &mut Floor) -> Self { let mut loop_index = 0; loop { @@ -179,13 +197,10 @@ impl Entity { 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.kind.move_speed()) { - // TODO: collision - self.fpos = fp; - } - } - + /// Handle movement for this entity. + /// + /// TODO: Merge this implementation (and Self::zombie_movement and Self::movement_helper) with the player movement + /// in lib.rs. pub fn handle_movement(&mut self, player_pos: Pos, floor: &mut Floor, delta_time: f32) { match &self.kind { EntityKind::Zombie(move_state) => { @@ -196,6 +211,7 @@ impl Entity { } } + /// State machine for zombie_movement. pub fn zombie_movement( &mut self, move_state: EnemyMoveState, @@ -233,24 +249,36 @@ impl Entity { self.kind = EntityKind::Zombie(EnemyMoveState::Roam(moves)); } - EnemyMoveState::Attack(mut moves) => { + EnemyMoveState::Attack(mut moves, old_player_pos) => { + if old_player_pos != player_pos { + self.kind = EntityKind::Zombie( + match EnemyMoveState::new_attack(self.pos, player_pos, floor) { + Some(move_state) => move_state, + None => EnemyMoveState::new_idle(), + }, + ); + return; + } + // 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, - ); + let next_move = *moves.last().unwrap_or(&Pos::default()); + + if next_move == self.pos { + moves.pop(); + } else { + Self::movement_helper(self, next_move, &mut moves, delta_time); + } - self.kind = EntityKind::Zombie(EnemyMoveState::Attack(moves)); + self.kind = EntityKind::Zombie(EnemyMoveState::Attack(moves, player_pos)); } } } + /// Simple movement_helper for enemy/player movement. fn movement_helper( &mut self, mut next_move: Pos, |