//! The `entity` module contains structures of all entities including players and enimies. use std::mem::take; use rand::Rng; use crate::{ Dungeon, astar, const_pos, map::Floor, player_input::PlayerInput, pos::{Direction, FPos, Pos}, rng::DungeonRng, }; /// `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, Default, PartialEq)] pub enum EntityKind { /// The main player #[default] 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 const fn move_speed(&self) -> f32 { match &self { Self::Player => 3.5, Self::Zombie(_) => 2.0, _ => 0.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 idle() -> Self { Self::Idle(0.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 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 roam(starting_pos: Pos, floor: &Floor, rng: &mut DungeonRng) -> Self { let mut loop_index = 0; loop { let dir = rng.random(); let dist = rng.random_range(MIN_ROAM_DIST..=MAX_ROAM_DIST); if let Some(p) = starting_pos.step_by(dir, dist) && !floor.get(p).is_wall() && 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::idle(); } } } /// Returns an optional reference to the current path this entity is moving pub const fn moves(&self) -> Option<&[Pos]> { match self { Self::Idle(_) => None, Self::Roam(moves) => Some(moves.as_slice()), Self::Attack(moves, _) => Some(moves.as_slice()), } } } impl Default for EnemyMoveState { fn default() -> Self { Self::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, /// The fixed grid position we are moving to pub moving_to: Option, } impl Entity { /// Creates a new `Entity` at a given `Pos`, `Direction`, and `EntityKind`. /// /// # Examples /// /// ``` /// use dungeon::{pos::{Pos, Direction}, entity::{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, moving_to: None, } } /// Creates the Player version of the `Entity` /// /// # Examples /// /// ``` /// use dungeon::{pos::Pos, entity::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::Pos, entity::Entity}; /// /// let pos = Pos::new(0, 0).unwrap(); /// let player = Entity::zombie(pos); /// ``` #[must_use] pub const fn zombie(pos: Pos) -> Self { let dir = Direction::East; let kind = EntityKind::Zombie(EnemyMoveState::idle()); let health = Some(ZOMBIE_HEALTH); Self::new(pos, dir, kind, health) } /// Returns the position in front of the entity pub const fn in_front(&self) -> Option { self.pos.step(self.dir) } /// Teleports a player directly to a `Pos` pub const fn teleport(&mut self, pos: Pos) { self.pos = pos; self.fpos = FPos::from_pos(pos); self.moving_to = None; } /// Returns a reference to this entities current AI pub const fn get_ai(&self) -> Option<&EnemyMoveState> { match &self.kind { EntityKind::Zombie(ai) => Some(ai), _ => None, } } } /// 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::Pos, entity::Player}; /// /// let pos = Pos::new(1, 2).unwrap(); /// let player = Player::new(pos); /// ``` pub const 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) } } struct Updater<'a> { floor: &'a Floor, rng: &'a mut DungeonRng, player_pos: Pos, input: PlayerInput, delta_time: f32, } impl Updater<'_> { fn update_entity(&mut self, entity: &mut Entity) { self.update_ai(entity); self.update_movement(entity); } fn update_ai(&mut self, entity: &mut Entity) { let mut kind = take(&mut entity.kind); match &mut kind { EntityKind::Player => { self.update_player_ai(entity); } EntityKind::Zombie(ai) => { self.update_enemy_ai(entity, ai); } _ => {} } entity.kind = kind; } /// Updates entity with player AI const fn update_player_ai(&self, entity: &mut Entity) { let Some(dir) = self.input.direction else { return; }; if entity.moving_to.is_some() { return; } entity.dir = dir; let Some(dest) = entity.pos.step(dir) else { return; }; if self.floor.get(dest).is_walkable() { entity.moving_to = Some(dest); } } /// Update entity with enemy AI fn update_enemy_ai(&mut self, entity: &mut Entity, ai: &mut EnemyMoveState) { use EnemyMoveState as State; // get the next position this entity may be moving to. // // we want to be able to short circut past pathed movement // if we generate a new path, without allowing the entity to move // off grid! thus this is the best position to start the new // movement from :) let next_pos = entity .in_front() .filter(|pos| self.floor.get(*pos).is_walkable()) .or(entity.moving_to) .unwrap_or(entity.pos); // check if player is in range if !matches!(ai, State::Attack { .. }) && let Some(m_state) = State::attack(next_pos, self.player_pos, self.floor) { *ai = m_state; entity.moving_to = Some(next_pos); return; } match ai { State::Idle(idle_accum) => { if *idle_accum < IDLE_WAIT { *ai = State::Idle(*idle_accum + self.delta_time); return; } *ai = State::roam(next_pos, self.floor, self.rng); entity.moving_to = Some(next_pos); } State::Roam(moves) => { if moves.is_empty() { *ai = State::idle(); return; } if entity.moving_to.is_none() { entity.moving_to = moves.pop(); } } State::Attack(moves, old_player_pos) => { if *old_player_pos != self.player_pos { *ai = State::attack(next_pos, self.player_pos, self.floor) .unwrap_or_default(); entity.moving_to = Some(next_pos); return; } *old_player_pos = self.player_pos; // Stop at one move left so the enemy is flush with the player tile if moves.len() == 1 { return; } while moves.last() == Some(&entity.pos) { moves.pop(); } if entity.moving_to.is_none() { entity.moving_to = moves.pop(); } } } } /// Update entity movement fn update_movement(&self, entity: &mut Entity) { let Some(dest) = entity.moving_to else { return }; let fdest = FPos::from_pos(dest); let current = entity.fpos; // we are far from the goal, so we need to move towards it let to_move = entity.kind.move_speed() * self.delta_time; entity.fpos.move_towards_manhattan(fdest, to_move); if fdest.abs_diff(entity.fpos).magnitude() <= 0.1 { // we have reached our destination (close enough) entity.moving_to = None; entity.pos = dest; entity.fpos = fdest; } if let Some(dir) = current.dir_to(entity.fpos) { entity.dir = dir; } } } impl Dungeon { pub(crate) fn update_entities(&mut self, input: PlayerInput, delta_time: f32) { let mut updater = Updater { floor: &self.floor, rng: &mut self.game_rng, player_pos: self.player.entity.pos, input, delta_time, }; updater.update_entity(&mut self.player.entity); for enemy in &mut self.enemies { updater.update_entity(enemy); } } }