use std::time::Instant; use crate::{ Dungeon, const_pos, entity::{Entity, EntityKind, Item}, map::{Floor, Tile}, msg::Message, player_input::PlayerInput, pos::{Direction, Pos}, }; /// `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; /// 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, } /// Timer #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] pub struct Timer { start: Option, secs: u64, } impl Timer { pub const fn new() -> Self { Self { start: None, secs: 0, } } /// Set the timer that triggers in a given amount of seconds pub fn set_secs(&mut self, secs: u64) { self.start = Some(Instant::now()); self.secs = secs; } /// Checks if the timer has finished pub fn finished(&self) -> bool { if let Some(start) = self.start { start.elapsed().as_secs() >= self.secs } else { false } } /// Returns if the timer is set pub const fn is_set(&self) -> bool { self.start.is_some() } /// Resets the timer pub const fn reset(&mut self) { *self = Self::new(); } } /// The current "weapon" level we have #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Weapon { RustyKnife, ShiningBlade, GodlyBlade, } impl Weapon { /// Returns the number of hit points of damage this weapon does #[must_use] pub const fn attack_dmg(self) -> u32 { match self { Self::RustyKnife => 2, Self::ShiningBlade => 3, Self::GodlyBlade => 5, } } } /// Represents the player inventory #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] pub struct Inventory { inner: [Option; PLAYER_INVENTORY_SIZE_USIZE], active: usize, } impl Inventory { pub const fn new() -> Self { let inner = [None; PLAYER_INVENTORY_SIZE_USIZE]; let active = 0; Self { inner, active } } /// Attemps to set the active inv slot pub const fn set_active(&mut self, active: usize) { if active < PLAYER_INVENTORY_SIZE_USIZE { self.active = active; } } /// Attemps to take an item based on the active inv slot. pub fn take(&mut self) -> Option { std::mem::take(&mut self.inner[self.active]) } /// Attemps to pickup an item from the ground. /// Returns if there was space for the item pub fn pickup(&mut self, item: Item) -> bool { for slot in &mut self.inner { if slot.is_none() { *slot = Some(item); return true; } } false } /// Returns an iterator over each inv slot, and if the slot is active pub fn iter(&self) -> impl Iterator, bool)> { self.inner.iter().enumerate().map(|(idx, item)| { let active = self.active == idx; (*item, active) }) } } /// The `Player` type represents the main player entity #[derive(Debug, Clone, PartialEq)] pub struct Player { pub entity: Entity, pub inventory: Inventory, pub weapon: Weapon, pub potion_timer: Timer, pub pickup_timer: Timer, } impl Player { /// Instantiates the game player at a given `Pos` /// /// # Examples /// /// ``` /// use dungeon::{pos::Pos, player::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 = Inventory::new(); let weapon = Weapon::RustyKnife; let potion_timer = Timer::new(); let pickup_timer = Timer::new(); Self { entity, inventory, weapon, potion_timer, pickup_timer, } } /// Reset potion effects if `potion_timer` is up fn update_potion_effects(&mut self) { if self.potion_timer.finished() { self.potion_timer.reset(); self.entity.speed = self.entity.kind.initial_speed(); } } } impl Default for Player { fn default() -> Self { let pos = const_pos!(1, 1); Self::new(pos) } } struct Updater<'a> { player: &'a mut Player, floor: &'a mut Floor, entities: &'a mut Vec, msg: &'a mut Message, input: PlayerInput, action: PlayerAction, } impl Updater<'_> { /// Handle "Use Item" action fn handle_use_item(&mut self) { if !self.input.use_item { return; } if let Some(item) = self.player.inventory.take() { self.action.potion = item.is_potion(); self.action.bomb = item.is_bomb(); item.consume(self.player, self.floor); } } /// Handle "Drop Item" action fn handle_drop_item(&mut self) { if !self.input.drop { return; } if let Some(item) = self.player.inventory.take() { self.entities.push(Entity::new( self.player.entity.pos, Direction::East, EntityKind::Item(item), )); self.action.drop_item = true; self.player.pickup_timer.set_secs(2); } } /// Handle "Pickup Item" action fn handle_pickup_item(&mut self) { if !self.player.pickup_timer.finished() && self.player.pickup_timer.is_set() { return; } self.player.pickup_timer.reset(); let mut idx = 0; loop { if idx >= self.entities.len() { break; } if self.entities[idx] .fpos .abs_diff(self.player.entity.fpos) .magnitude() >= 0.25 { idx += 1; continue; } let Some(item) = self.entities[idx].get_item() else { idx += 1; continue; }; if self.player.inventory.pickup(item) { self.entities.remove(idx); self.action.pickup_item = true; } else { idx += 1; } } } /// Handle "Player Attack" action const fn handle_attack(&mut self) { if self.input.attack { // TODO: attack self.action.attack = true; } } /// Handle "Player Interact" action fn handle_interact(&mut self) { if !self.input.interact { return; } // check for chests if let Tile::Chest(opt) = self.floor.get_mut(self.player.entity.pos) && let Some(item) = *opt && self.player.inventory.pickup(item) { *opt = None; self.msg .set_message(&format!("Woah a chest! You found a '{item}'!")); } } /// Handle "Inv Slot" action const fn handle_inv_slot(&mut self) { if let Some(slot) = self.input.inv_slot { self.player.inventory.set_active(slot); } } } impl Dungeon { pub(crate) fn update_player(&mut self, input: PlayerInput) -> PlayerAction { let mut updater = Updater { player: &mut self.player, floor: &mut self.floor, entities: &mut self.entities, msg: &mut self.msg, input, action: PlayerAction::default(), }; updater.player.update_potion_effects(); updater.handle_use_item(); updater.handle_drop_item(); updater.handle_pickup_item(); updater.handle_attack(); updater.handle_interact(); updater.handle_inv_slot(); updater.action } }