//! The `entity` module contains structures of all entities including players and enimies. use rand::Rng; use crate::{Direction, FPos, Floor, Pos, astar, const_pos}; /// `PLAYER_FULL_HEALTH` is the starting health of the player entity pub const PLAYER_FULL_HEALTH: u32 = 10; /// `PLAYER_INVENTORY_SIZE` is the maximum size of the inventory 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 { Potion { heal: u32 }, Weapon { atack: u32 }, Armor { defense: u32 }, } /// The `EntityKind` represents what kind of entity this is. #[derive(Clone, Debug, PartialEq)] pub enum EntityKind { /// The main player Player, Zombie(EnemyMoveState), /// An item (not in an inventory) on the floor of the dungeon Item(Item), } impl EntityKind { /// Returns the move speed value for this type of entity in tiles/s pub fn move_speed(&self) -> f32 { match &self { Self::Player => 5., Self::Zombie(_) => 4., _ => 0., } } } /// 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` 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), Attack(Vec, 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 { 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, 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 { let dir = Direction::random(floor.rand()); let dist = floor.rand().random_range(MIN_ROAM_DIST..=MAX_ROAM_DIST); if let Some(p) = starting_pos.step_by(dir, dist) { if !floor.get(p).is_wall() { if let Some(data) = astar::astar(starting_pos, p, floor) { let mut path = data.0; path.reverse(); return Self::Roam(path); } } } // Safety check loop_index += 1; if loop_index >= 100 { return Self::new_idle(); } } } } /// The `Entity` kind represents the main player, or any other /// ai autonomous character that can move freely across the /// dungeon. #[derive(Clone, Debug, PartialEq)] pub struct Entity { /// The fixed grid position of the entity pub pos: Pos, /// The floating (real) current position of the entity pub fpos: FPos, /// Which direction this entity is facing pub dir: Direction, /// Which kind this entity is (along with entity kind specific data) pub kind: EntityKind, /// The amount of health this entity has (None if this Entity does not use health) pub health: Option, } impl Entity { /// Creates a new `Entity` at a given `Pos`, `Direction`, and `EntityKind`. /// /// # Examples /// /// ``` /// 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 entity = Entity::new(pos, dir, kind, health); /// ``` #[must_use] pub const fn new( pos: Pos, dir: Direction, kind: EntityKind, health: Option, ) -> Self { let fpos = FPos::from_pos(pos); Self { pos, fpos, dir, kind, health, } } /// Creates the Player version of the `Entity` /// /// # Examples /// /// ``` /// use dungeon::{Pos, Entity}; /// /// let pos = Pos::new(0, 0).unwrap(); /// let player = Entity::player(pos); /// ``` #[must_use] pub const fn player(pos: Pos) -> Self { let dir = Direction::East; let kind = EntityKind::Player; let health = Some(PLAYER_FULL_HEALTH); Self::new(pos, dir, kind, health) } /// Creates the Zombie version of the `Entity` /// /// # Examples /// /// ``` /// use dungeon::{Pos, Entity}; /// /// let pos = Pos::new(0, 0).unwrap(); /// let player = Entity::zombie(pos); /// ``` pub const fn zombie(pos: Pos) -> Self { let dir = Direction::East; let kind = EntityKind::Zombie(EnemyMoveState::new_idle()); let health = Some(ZOMBIE_HEALTH); Self::new(pos, dir, kind, health) } /// 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) => { self.zombie_movement(move_state.clone(), player_pos, delta_time, floor); } EntityKind::Player => {} _ => {} } } /// State machine for zombie_movement. pub fn zombie_movement( &mut self, move_state: EnemyMoveState, player_pos: Pos, delta_time: f32, floor: &mut Floor, ) { // Check if player is in range if !matches!(move_state, EnemyMoveState::Attack { .. }) && let Some(m_state) = EnemyMoveState::new_attack(self.pos, player_pos, floor) { 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, floor)); } 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, 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; } 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, player_pos)); } } } /// Simple movement_helper for enemy/player movement. fn movement_helper( &mut self, mut next_move: Pos, moves: &mut Vec, delta_time: f32, ) { let mut move_distance = self.kind.move_speed() * delta_time; // having this be a loop is a *little* unnecessary, // but is technically more correct if the entity is fast enough // to move more than one tile in a single frame loop { move_distance -= self .fpos .move_towards_manhattan(FPos::from(next_move), move_distance); if move_distance == 0.0 { // can't move any further break; } // otherwise, we reached `next_move`, set position & pop from vec self.pos = next_move; let _ = moves.pop(); // there is likely more distance to travel let Some(last) = moves.last() else { break }; next_move = *last; } } } /// The `Player` type represents the main player entity #[derive(Clone, Debug, PartialEq)] pub struct Player { pub moving_to: Option, pub entity: Entity, pub inventory: Vec, } impl Player { /// Instantiates the game player at a given `Pos` /// /// # Examples /// /// ``` /// use dungeon::{Pos, Player}; /// /// let pos = Pos::new(1, 2).unwrap(); /// let player = Player::new(pos); /// ``` pub fn new(pos: Pos) -> Self { let entity = Entity::player(pos); let inventory = vec![]; Self { entity, inventory, moving_to: None, } } } impl Default for Player { fn default() -> Self { let pos = const_pos!(1, 1); Self::new(pos) } }