//! The `entity` module contains structures of all entities including players and enimies. 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; /// `PLAYER_INVENTORY_SIZE` is the maximum size of the inventory pub const PLAYER_INVENTORY_SIZE: usize = 5; /// 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 { // Tiles/s pub fn move_speed(&self) -> f32 { match &self { 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), Attack(Vec), } impl EnemyMoveState { pub const 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; 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) } 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; } } 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, 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 #[derive(Clone, Debug, PartialEq)] pub struct Player { 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 } } } impl Default for Player { fn default() -> Self { let pos = const_pos!(1, 1); Self::new(pos) } }