diff options
| author | Freya Murphy <freya@freyacat.org> | 2025-11-20 18:23:20 -0500 |
|---|---|---|
| committer | Freya Murphy <freya@freyacat.org> | 2025-11-20 18:23:20 -0500 |
| commit | 2d4644889dafa69c06e284b8b2cd18d2f65e57bc (patch) | |
| tree | c26f19e9eedc845d3bfd86557880257af334f0cf /dungeon/src/player.rs | |
| parent | dungeon: make chest items optional (no item = open) (diff) | |
| download | DungeonCrawl-2d4644889dafa69c06e284b8b2cd18d2f65e57bc.tar.gz DungeonCrawl-2d4644889dafa69c06e284b8b2cd18d2f65e57bc.tar.bz2 DungeonCrawl-2d4644889dafa69c06e284b8b2cd18d2f65e57bc.zip | |
dungeon: refactor player out of entity.rs
Diffstat (limited to 'dungeon/src/player.rs')
| -rw-r--r-- | dungeon/src/player.rs | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/dungeon/src/player.rs b/dungeon/src/player.rs new file mode 100644 index 0000000..34b0ab1 --- /dev/null +++ b/dungeon/src/player.rs @@ -0,0 +1,294 @@ +use std::time::Instant; + +use crate::{ + Dungeon, const_pos, + entity::{Entity, EntityKind, Item}, + map::Floor, + 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<Instant>, + 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<Item>; 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<Item> { + std::mem::take(&mut self.inner[self.active]) + } + + /// Attemps to pickup an item from the ground, returning the + /// item if the inventory is full + pub fn pickup(&mut self, item: Item) -> Option<Item> { + for slot in &mut self.inner { + if slot.is_none() { + *slot = Some(item); + return None; + } + } + Some(item) + } + + /// Returns an iterator over each inv slot, and if the slot is active + pub fn iter(&self) -> impl Iterator<Item = (Option<Item>, 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<Entity>, + 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(5); + } + } + + /// 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).is_none() { + 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 "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, + 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_inv_slot(); + + updater.action + } +} |