//! The `dungon` crate contains the core functionality for //! interacting with a `Dungeon` and its components. pub mod astar; pub mod bsp; pub mod entity; pub mod map; pub mod msg; pub mod player_input; pub mod pos; pub mod rng; use std::time::Instant; use rand::{Rng, SeedableRng, TryRngCore, rngs::OsRng}; use crate::{ entity::{Entity, EntityKind, PLAYER_INVENTORY_SIZE_USIZE, Player}, map::{Floor, Tile}, msg::Message, player_input::PlayerInput, pos::{Direction, FPos}, rng::DungeonRng, }; /// Lets the caller know what actions /// exactly the player has done #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct PlayerAction { // The player picked up an item pub pickup_item: bool, // The player dropped an item pub drop_item: bool, // The player moved pub walk: bool, // The player attacked pub attack: bool, // The player used a bomb pub bomb: bool, // The player used a potion pub potion: bool, } /// Lets the caller know what has /// changed in the game state this /// tick #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum UpdateResult { /// Default, entities have moved, player done stuff Default(PlayerAction), /// We have moved to the next floor NextFloor, /// Message on screen updated. /// Contains if a char was added. MessageUpdated(bool), /// Game over :( GameOver, /// We have done nothing! Nothing, } /// The `Dungeon` type represents the game state of the /// dungeon crawler. #[derive(Debug, Clone)] pub struct Dungeon { pub floor: Floor, pub player: Player, pub entities: Vec, pub msg: Message, seed: u64, level_rng: DungeonRng, game_rng: DungeonRng, game_over: bool, } impl Dungeon { /// Creates a new `Dungeon` with a provided seed. /// /// # Examples /// /// ```no_run /// use dungeon::Dungeon; /// /// let seed = 234690523482u64; /// let dungeon = Dungeon::new(seed); /// ``` #[must_use] pub fn new(seed: u64) -> Self { let mut game_rng = DungeonRng::seed_from_u64(seed); let mut level_rng = DungeonRng::seed_from_u64(game_rng.random()); let floor = bsp::generate(&mut level_rng); let player = Player::new(floor.player_start()); let entities = vec![]; let msg = Message::empty(); let game_over = false; let mut dungeon = Self { floor, player, entities, msg, seed, level_rng, game_rng, game_over, }; dungeon.spawn_enimies(); dungeon } /// Creates a new `Dungeon` with a random seed /// /// # Examples /// /// ```no_run /// use dungeon::Dungeon; /// /// let dungeon = Dungeon::random(); /// ``` #[must_use] pub fn random() -> Self { let seed = OsRng.try_next_u64().unwrap_or_default(); Self::new(seed) } /// Returns the current position of the camera (viewer) #[must_use] pub const fn camera(&self) -> FPos { self.player.entity.fpos } /// Returns the seed used to generate the map #[must_use] pub const fn seed(&self) -> u64 { self.seed } /// Returns the runtime random number gen for the `Floor` #[must_use] pub const fn rng(&mut self) -> &mut DungeonRng { &mut self.game_rng } pub fn update(&mut self, player_input: PlayerInput, delta_time: f32) -> UpdateResult { if self.game_over { UpdateResult::Nothing } else if self.msg.visible() { let changed = self.msg.update(player_input); UpdateResult::MessageUpdated(changed) } else { let mut action = self.update_player(player_input); let curr_player_pos = self.player.entity.fpos; self.update_entities(player_input, delta_time); if self .player .entity .fpos .abs_diff(curr_player_pos) .magnitude() > 0.0 { // player has moved action.walk = true; } if self.player.entity.health < 1 { // player has died :( self.game_over = true; return UpdateResult::GameOver; } if self.floor.get(self.player.entity.pos) == Tile::Stairs { // we are moving to a new floor self.floor = bsp::generate(&mut self.level_rng); self.player.entity.teleport(self.floor.player_start()); self.entities.clear(); self.spawn_enimies(); return UpdateResult::NextFloor; } UpdateResult::Default(action) } } fn update_player(&mut self, player_input: PlayerInput) -> PlayerAction { let mut action = PlayerAction::default(); // update potion timer if let Some(timer) = self.player.potion_timer && Instant::now() > timer { self.player.potion_timer = None; self.player.entity.speed = self.player.entity.kind.initial_speed(); } // use item if player_input.use_item && self.player.active_inv_slot < self.player.inventory.len() { let item = self.player.inventory.remove(self.player.active_inv_slot); action.potion = item.is_potion(); action.bomb = item.is_bomb(); item.consume(self); } if player_input.attack { // TODO: attack action.attack = true; } // check for drop input if player_input.drop && self.player.active_inv_slot < self.player.inventory.len() { let item = self.player.inventory.remove(self.player.active_inv_slot); self.entities.push(Entity::new( self.player.entity.pos, Direction::East, EntityKind::Item(item), )); self.player.active_inv_slot = self .player .active_inv_slot .max(self.player.inventory.len().saturating_sub(1)); action.drop_item = true; } // TODO: pickup items // check for inv slot request if let Some(slot) = player_input.inv_slot && slot < PLAYER_INVENTORY_SIZE_USIZE { self.player.active_inv_slot = slot; } action } fn spawn_enimies(&mut self) { // TODO: better entity spawning let zombie = Entity::zombie(self.floor.random_walkable_pos(&mut self.level_rng)); self.entities.push(zombie); } }