summaryrefslogtreecommitdiff
path: root/dungeon
diff options
context:
space:
mode:
Diffstat (limited to 'dungeon')
-rw-r--r--dungeon/src/entity.rs291
-rw-r--r--dungeon/src/lib.rs79
-rw-r--r--dungeon/src/pos.rs16
3 files changed, 202 insertions, 184 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)
- }
-}
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<Direction> {
+ 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<Self> for FPos {