From 583b733520171b9182e01bbe90bf3ec5da0de428 Mon Sep 17 00:00:00 2001 From: Freya Murphy Date: Sat, 15 Nov 2025 01:59:52 -0500 Subject: dungeon: refactor entity movement --- dungeon/src/entity.rs | 293 ++++++++++++++++++++++++++++---------------------- dungeon/src/lib.rs | 79 ++++---------- dungeon/src/pos.rs | 16 +++ 3 files changed, 203 insertions(+), 185 deletions(-) (limited to 'dungeon') 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 { + 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; @@ -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, + /// 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`. @@ -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 { + 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, +} +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; } - /// 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) + /// 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; + + // 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, - 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 }; - // there is likely more distance to travel - let Some(last) = moves.last() else { break }; - next_move = *last; - } - } + let fdest = FPos::from_pos(dest); + let current = entity.fpos; - /// Returns the position in front of the entity - pub const fn in_front(&self) -> Option { - self.pos.step(self.dir) - } -} + // 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); -/// 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::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, + 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 Default for Player { - fn default() -> Self { - let pos = const_pos!(1, 1); - Self::new(pos) +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, + }; + + updater.update_entity(&mut self.player.entity); + for enemy in &mut self.enemies { + updater.update_entity(enemy); + } } } diff --git a/dungeon/src/lib.rs b/dungeon/src/lib.rs index 0c9e055..036671c 100644 --- a/dungeon/src/lib.rs +++ b/dungeon/src/lib.rs @@ -16,7 +16,7 @@ use rand::{ use crate::{ entity::{Entity, Player}, - map::Floor, + map::{Floor, Tile}, msg::Message, player_input::PlayerInput, pos::FPos, @@ -29,6 +29,8 @@ use crate::{ pub enum UpdateResult { /// Default, entities have moved EntityMovement, + /// We have moved to the next floor + NextFloor, /// Message on screen updated. /// Contains if a char was added. MessageUpdated(bool), @@ -61,18 +63,19 @@ impl Dungeon { let mut rng = SmallRng::seed_from_u64(seed); let floor = bsp::generate(&mut rng); let player = Player::new(floor.player_start()); - // TODO: Randomize enemy positions/types - let enemies = vec![Entity::zombie(floor.random_walkable_pos(&mut rng))]; + let enemies = vec![]; let msg = Message::empty(); - - Self { + let mut dungeon = Self { floor, player, enemies, msg, seed, rng, - } + }; + + dungeon.spawn_enimies(); + dungeon } /// Creates a new `Dungeon` with a random seed @@ -113,60 +116,24 @@ impl Dungeon { let changed = self.msg.update(player_input); UpdateResult::MessageUpdated(changed) } else { - self.act_player(player_input, delta_time); - self.act_non_players(delta_time); - UpdateResult::EntityMovement - } - } + self.update_entities(player_input, delta_time); - fn act_player(&mut self, player_input: PlayerInput, delta_time: f32) { - let mut move_distance = self.player.entity.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 - // (plus, it was easier to write if i thought of this like a state machine) - loop { - match (self.player.moving_to, player_input.direction) { - (Some(pos), _) => { - move_distance -= self - .player - .entity - .fpos - .move_towards_manhattan(pos.into(), move_distance); - if move_distance == 0.0 { - // can't move any further - break; - } - // otherwise, reached `pos` - self.player.entity.pos = pos; - self.player.moving_to = None; - // continue moving - } - (None, Some(dir)) => { - // set direction & find out next position - self.player.entity.dir = dir; - if let Some(pos) = self.player.entity.pos.step(dir) - && self.floor.get(pos).is_walkable() - { - self.player.moving_to = Some(pos); - } else { - break; - } - } - (None, None) => break, + if self.floor.get(self.player.entity.pos) == Tile::Stairs { + // we are moving to a new floor + self.floor = bsp::generate(&mut self.rng); + self.player.entity.teleport(self.floor.player_start()); + self.spawn_enimies(); + return UpdateResult::NextFloor; } + + UpdateResult::EntityMovement } - // TODO: reuse a similar structure across all entities } - fn act_non_players(&mut self, delta_time: f32) { - for enemy in &mut self.enemies { - enemy.handle_movement( - self.player.entity.pos, - &self.floor, - &mut self.rng, - delta_time, - ); - } + fn spawn_enimies(&mut self) { + // TODO: better entity spawning + let zombie = Entity::zombie(self.floor.random_walkable_pos(&mut self.rng)); + self.enemies.clear(); + self.enemies.push(zombie); } } diff --git a/dungeon/src/pos.rs b/dungeon/src/pos.rs index 7ee97eb..8b6d199 100644 --- a/dungeon/src/pos.rs +++ b/dungeon/src/pos.rs @@ -11,6 +11,7 @@ use rand::{ }; use std::{ + cmp::Ordering, fmt::Display, ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}, }; @@ -663,6 +664,21 @@ impl FPos { let abs_diff = Self::abs_diff(self, other); abs_diff.0 + abs_diff.1 } + + /// Returns the direction the position is from the other + #[must_use] + pub fn dir_to(self, other: Self) -> Option { + let x_ord = self.x().partial_cmp(&other.x())?; + let y_ord = self.y().partial_cmp(&other.y())?; + + match (x_ord, y_ord) { + (Ordering::Less, Ordering::Equal) => Some(Direction::East), + (Ordering::Greater, Ordering::Equal) => Some(Direction::West), + (Ordering::Equal, Ordering::Less) => Some(Direction::South), + (Ordering::Equal, Ordering::Greater) => Some(Direction::North), + _ => None, + } + } } impl AddAssign for FPos { -- cgit v1.2.3-freya