summaryrefslogtreecommitdiff
path: root/dungeon/src/entity.rs
diff options
context:
space:
mode:
Diffstat (limited to 'dungeon/src/entity.rs')
-rw-r--r--dungeon/src/entity.rs291
1 files changed, 163 insertions, 128 deletions
diff --git a/dungeon/src/entity.rs b/dungeon/src/entity.rs
index f56f8dd..6ed46cc 100644
--- a/dungeon/src/entity.rs
+++ b/dungeon/src/entity.rs
@@ -1,10 +1,13 @@
//! The `entity` module contains structures of all entities including players and enimies.
+use std::mem::take;
+
use rand::{Rng, rngs::SmallRng};
use crate::{
- astar, const_pos,
+ Dungeon, astar, const_pos,
map::Floor,
+ player_input::PlayerInput,
pos::{Direction, FPos, Pos},
};
@@ -58,6 +61,11 @@ impl EntityKind {
}
}
}
+impl Default for EntityKind {
+ fn default() -> Self {
+ Self::Player
+ }
+}
/// 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
@@ -72,15 +80,15 @@ pub enum EnemyMoveState {
impl EnemyMoveState {
/// Creates an `EnemyMoveState::Idle` enum, initialized to a time accumlation of 0.
- pub const fn new_idle() -> Self {
- Self::Idle(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 new_attack(starting_pos: Pos, player_pos: Pos, floor: &Floor) -> Option<Self> {
+ pub fn 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;
@@ -94,7 +102,7 @@ impl EnemyMoveState {
///
/// 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: &Floor, rng: &mut SmallRng) -> Self {
+ pub fn roam(starting_pos: Pos, floor: &Floor, rng: &mut SmallRng) -> Self {
let mut loop_index = 0;
loop {
let dir = rng.random();
@@ -113,11 +121,16 @@ impl EnemyMoveState {
// Safety check
loop_index += 1;
if loop_index >= 100 {
- return Self::new_idle();
+ return Self::idle();
}
}
}
}
+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
@@ -134,6 +147,8 @@ pub struct Entity {
pub kind: EntityKind,
/// The amount of health this entity has (None if this Entity does not use health)
pub health: Option<u32>,
+ /// The fixed grid position we are moving to
+ pub moving_to: Option<Pos>,
}
impl Entity {
/// Creates a new `Entity` at a given `Pos`, `Direction`, and `EntityKind`.
@@ -163,6 +178,7 @@ impl Entity {
dir,
kind,
health,
+ moving_to: None,
}
}
@@ -197,170 +213,189 @@ impl Entity {
#[must_use]
pub const fn zombie(pos: Pos) -> Self {
let dir = Direction::East;
- let kind = EntityKind::Zombie(EnemyMoveState::new_idle());
+ let kind = EntityKind::Zombie(EnemyMoveState::idle());
let health = Some(ZOMBIE_HEALTH);
Self::new(pos, dir, kind, health)
}
- /// Handle movement for this entity.
+ /// Returns the position in front of the entity
+ pub const fn in_front(&self) -> Option<Pos> {
+ 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;
+ }
+}
+
+/// The `Player` type represents the main player entity
+#[derive(Clone, Debug, PartialEq)]
+pub struct Player {
+ pub entity: Entity,
+ pub inventory: Vec<Item>,
+}
+impl Player {
+ /// Instantiates the game player at a given `Pos`
///
- /// 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: &Floor,
- rng: &mut SmallRng,
- delta_time: f32,
- ) {
- match &self.kind {
- EntityKind::Zombie(move_state) => {
- self.zombie_movement(
- move_state.clone(),
- player_pos,
- delta_time,
- floor,
- rng,
- );
+ /// # 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 SmallRng,
+ 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);
}
- EntityKind::Player => {}
_ => {}
}
+ 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);
+ }
}
- /// State machine for zombie_movement.
- pub fn zombie_movement(
- &mut self,
- move_state: EnemyMoveState,
- player_pos: Pos,
- delta_time: f32,
- floor: &Floor,
- rng: &mut SmallRng,
- ) {
- // Check if player is in range
- if !matches!(move_state, EnemyMoveState::Attack { .. })
- && let Some(m_state) = EnemyMoveState::new_attack(self.pos, player_pos, floor)
+ /// Update entity with enemy AI
+ fn update_enemy_ai(&mut self, entity: &mut Entity, ai: &mut EnemyMoveState) {
+ use EnemyMoveState as State;
+
+ // check if player is in range
+ if !matches!(ai, State::Attack { .. })
+ && let Some(m_state) = State::attack(entity.pos, self.player_pos, self.floor)
{
- self.kind = EntityKind::Zombie(m_state);
+ *ai = 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));
+ match ai {
+ State::Idle(idle_accum) => {
+ if *idle_accum < IDLE_WAIT {
+ *ai = State::Idle(*idle_accum + self.delta_time);
return;
}
- self.kind =
- EntityKind::Zombie(EnemyMoveState::new_roam(self.pos, floor, rng));
+ *ai = State::roam(entity.pos, self.floor, self.rng);
}
- EnemyMoveState::Roam(mut moves) => {
- let Some(p) = moves.last() else {
- self.kind = EntityKind::Zombie(EnemyMoveState::new_idle());
+ State::Roam(moves) => {
+ if moves.is_empty() {
+ *ai = State::idle();
return;
- };
-
- Self::movement_helper(self, *p, &mut moves, delta_time);
+ }
- self.kind = EntityKind::Zombie(EnemyMoveState::Roam(moves));
+ if entity.moving_to.is_none() {
+ entity.moving_to = moves.pop();
+ }
}
- 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(),
- },
- );
+ State::Attack(moves, old_player_pos) => {
+ if *old_player_pos != self.player_pos {
+ *ai = State::attack(entity.pos, self.player_pos, self.floor)
+ .unwrap_or_default();
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;
}
- let next_move = *moves.last().unwrap_or(&Pos::default());
-
- if next_move == self.pos {
+ while moves.last() == Some(&entity.pos) {
moves.pop();
- } else {
- Self::movement_helper(self, next_move, &mut moves, delta_time);
}
- self.kind = EntityKind::Zombie(EnemyMoveState::Attack(moves, player_pos));
+ if entity.moving_to.is_none() {
+ entity.moving_to = moves.pop();
+ }
}
}
}
- /// Simple movement_helper for enemy/player movement.
- fn movement_helper(
- &mut self,
- mut next_move: Pos,
- moves: &mut Vec<Pos>,
- 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();
+ /// 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);
- // there is likely more distance to travel
- let Some(last) = moves.last() else { break };
- next_move = *last;
+ 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;
}
- }
- /// Returns the position in front of the entity
- pub const fn in_front(&self) -> Option<Pos> {
- self.pos.step(self.dir)
+ 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.rng,
+ player_pos: self.player.entity.pos,
+ input,
+ delta_time,
+ };
-/// The `Player` type represents the main player entity
-#[derive(Clone, Debug, PartialEq)]
-pub struct Player {
- pub moving_to: Option<Pos>,
- pub entity: Entity,
- pub inventory: Vec<Item>,
-}
-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,
- moving_to: None,
+ updater.update_entity(&mut self.player.entity);
+ for enemy in &mut self.enemies {
+ updater.update_entity(enemy);
}
}
}
-impl Default for Player {
- fn default() -> Self {
- let pos = const_pos!(1, 1);
- Self::new(pos)
- }
-}