From 2d4644889dafa69c06e284b8b2cd18d2f65e57bc Mon Sep 17 00:00:00 2001 From: Freya Murphy Date: Thu, 20 Nov 2025 18:23:20 -0500 Subject: dungeon: refactor player out of entity.rs --- dungeon/src/entity.rs | 206 +--------------------------------- dungeon/src/lib.rs | 24 +--- dungeon/src/player.rs | 294 +++++++++++++++++++++++++++++++++++++++++++++++++ game/src/lib.rs | 10 +- graphics/src/lib.rs | 3 +- graphics/src/render.rs | 23 ++-- 6 files changed, 314 insertions(+), 246 deletions(-) create mode 100644 dungeon/src/player.rs diff --git a/dungeon/src/entity.rs b/dungeon/src/entity.rs index d6ca7e5..5b4ac38 100644 --- a/dungeon/src/entity.rs +++ b/dungeon/src/entity.rs @@ -1,26 +1,18 @@ //! The `entity` module contains structures of all entities including players and enimies. -use std::{ - mem::take, - time::{Duration, Instant}, -}; +use std::mem::take; use rand::Rng; use crate::{ - Dungeon, PlayerAction, astar, const_pos, + Dungeon, astar, map::Floor, + player::Player, player_input::PlayerInput, pos::{Direction, FPos, Pos}, rng::DungeonRng, }; -/// `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; - /// 'MIN_ROAM_DIST' and 'MAX_ROAM_DIST' are the enemy roam ranges pub const MIN_ROAM_DIST: u16 = 1; pub const MAX_ROAM_DIST: u16 = 4; @@ -55,7 +47,7 @@ impl Item { player.entity.heal(3); } Self::SpeedPotion => { - player.potion_timer = Instant::now().checked_add(Duration::from_secs(5)); + player.potion_timer.set_secs(5); player.entity.speed = player.entity.speed.inc(); } Self::Bomb => { @@ -339,73 +331,8 @@ impl Entity { } } -/// The current "weapon" level we have -#[derive(Clone, Copy, Debug, 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, - } - } -} - -/// The `Player` type represents the main player entity -#[derive(Clone, Debug, PartialEq)] -pub struct Player { - pub entity: Entity, - pub inventory: Vec, - pub weapon: Weapon, - // How long until we reset potion effects? - pub potion_timer: Option, - pub active_inv_slot: usize, - pub last_drop_time: Instant, -} -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 fn new(pos: Pos) -> Self { - let entity = Entity::player(pos); - let inventory = vec![]; - let weapon = Weapon::RustyKnife; - let potion_timer = None; - let active_inv_slot = 0; - let last_drop_time = Instant::now(); - Self { - entity, - inventory, - weapon, - potion_timer, - active_inv_slot, - last_drop_time, - } - } -} -impl Default for Player { - fn default() -> Self { - let pos = const_pos!(1, 1); - Self::new(pos) - } -} - struct Updater<'a> { - floor: &'a mut Floor, + floor: &'a Floor, rng: &'a mut DungeonRng, player_pos: Pos, input: PlayerInput, @@ -540,104 +467,11 @@ impl Updater<'_> { entity.dir = dir; } } - - /// Update player potion timer - fn update_player_potion_effects(player: &mut Player) { - if let Some(timer) = player.potion_timer - && Instant::now() > timer - { - player.potion_timer = None; - player.entity.speed = player.entity.kind.initial_speed(); - } - } - - /// Handle player "Use Item" - fn handle_player_use_item(&mut self, player: &mut Player, action: &mut PlayerAction) { - if self.input.use_item && player.active_inv_slot < player.inventory.len() { - let item = player.inventory.remove(player.active_inv_slot); - action.potion = item.is_potion(); - action.bomb = item.is_bomb(); - item.consume(player, self.floor); - } - } - - /// Handle player "Drop Item" - fn handle_player_drop_item( - &self, - player: &mut Player, - action: &mut PlayerAction, - entities: &mut Vec, - ) { - if self.input.drop && player.active_inv_slot < player.inventory.len() { - let item = player.inventory.remove(player.active_inv_slot); - entities.push(Entity::new( - player.entity.pos, - Direction::East, - EntityKind::Item(item), - )); - player.active_inv_slot = player - .active_inv_slot - .max(player.inventory.len().saturating_sub(1)); - action.drop_item = true; - player.last_drop_time = Instant::now(); - } - } - - /// Handle player "Attack" - const fn handle_player_attack(&self, _player: &mut Player, action: &mut PlayerAction) { - if self.input.attack { - // TODO: attack - action.attack = true; - } - } - - /// Handle player "Pickup Items" - fn handle_player_pickup_items( - player: &mut Player, - action: &mut PlayerAction, - entities: &mut Vec, - ) { - if player.last_drop_time.elapsed() < Duration::from_secs(3) { - // delay picking up dropped items - return; - } - - let mut idx = 0; - loop { - if idx >= entities.len() { - break; - } - if entities[idx].fpos.abs_diff(player.entity.fpos).magnitude() >= 0.25 { - idx += 1; - continue; - } - let Some(item) = entities[idx].get_item() else { - idx += 1; - continue; - }; - if player.inventory.len() < PLAYER_INVENTORY_SIZE_USIZE { - entities.remove(idx); - player.inventory.push(item); - action.pickup_item = true; - } else { - idx += 1; - } - } - } - - /// Handle player "Change INV Slot" - const fn handle_player_inv_slot(&self, player: &mut Player) { - if let Some(slot) = self.input.inv_slot - && slot < PLAYER_INVENTORY_SIZE_USIZE - { - player.active_inv_slot = slot; - } - } } impl Dungeon { pub(crate) fn update_entities(&mut self, input: PlayerInput, delta_time: f32) { let mut updater = Updater { - floor: &mut self.floor, + floor: &self.floor, rng: &mut self.game_rng, player_pos: self.player.entity.pos, input, @@ -654,32 +488,4 @@ impl Dungeon { } self.entities.retain(Entity::is_alive); } - - pub(crate) fn update_player( - &mut self, - input: PlayerInput, - delta_time: f32, - ) -> PlayerAction { - let mut updater = Updater { - floor: &mut self.floor, - rng: &mut self.game_rng, - player_pos: self.player.entity.pos, - input, - delta_time, - }; - - let mut action = PlayerAction::default(); - Updater::update_player_potion_effects(&mut self.player); - updater.handle_player_use_item(&mut self.player, &mut action); - updater.handle_player_drop_item(&mut self.player, &mut action, &mut self.entities); - Updater::handle_player_pickup_items( - &mut self.player, - &mut action, - &mut self.entities, - ); - updater.handle_player_attack(&mut self.player, &mut action); - updater.handle_player_inv_slot(&mut self.player); - - action - } } diff --git a/dungeon/src/lib.rs b/dungeon/src/lib.rs index fa75c10..91ab378 100644 --- a/dungeon/src/lib.rs +++ b/dungeon/src/lib.rs @@ -6,6 +6,7 @@ pub mod bsp; pub mod entity; pub mod map; pub mod msg; +pub mod player; pub mod player_input; pub mod pos; pub mod rng; @@ -13,32 +14,15 @@ pub mod rng; use rand::{Rng, SeedableRng, TryRngCore, rngs::OsRng}; use crate::{ - entity::{Entity, Player}, + entity::Entity, map::{Floor, Tile}, msg::Message, + player::{Player, PlayerAction}, player_input::PlayerInput, pos::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 @@ -145,7 +129,7 @@ impl Dungeon { let changed = self.msg.update(player_input); UpdateResult::MessageUpdated(changed) } else { - let mut action = self.update_player(player_input, delta_time); + 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 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, + 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, returning the + /// item if the inventory is full + pub fn pickup(&mut self, item: Item) -> Option { + 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, 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, + 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 + } +} diff --git a/game/src/lib.rs b/game/src/lib.rs index b6d2fa4..8465ff3 100644 --- a/game/src/lib.rs +++ b/game/src/lib.rs @@ -1,7 +1,4 @@ -use dungeon::{ - Dungeon, UpdateResult, entity::PLAYER_INVENTORY_SIZE, player_input::PlayerInput, - pos::Direction, -}; +use dungeon::{Dungeon, UpdateResult, player_input::PlayerInput, pos::Direction}; use graphics::{Key, Window}; pub struct Game { @@ -66,10 +63,7 @@ impl Game { let use_item = self.window.is_key_pressed(Key::E); let attack = self.window.is_key_pressed(Key::F); let drop = self.window.is_key_pressed(Key::Q); - let inv_slot = (0..PLAYER_INVENTORY_SIZE) - .filter_map(|u16| u8::try_from(u16).ok()) - .find(|u8| self.window.is_key_pressed(Key::Number(*u8 + 1))) - .map(|u8| u8 as usize); + let inv_slot = (0..9).find(|n| self.window.is_key_pressed(Key::Number(*n))); PlayerInput { direction, interact, diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 02eb97f..fef5f9e 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -19,7 +19,6 @@ pub type Error = Box; /// The `Result` type used witin this crate pub type Result = std::result::Result; -#[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Key { // Movement @@ -38,7 +37,7 @@ pub enum Key { F, Q, // Number - Number(u8), + Number(usize), // Debug keys F3, F4, diff --git a/graphics/src/render.rs b/graphics/src/render.rs index 613763c..762827c 100644 --- a/graphics/src/render.rs +++ b/graphics/src/render.rs @@ -7,8 +7,9 @@ use std::{ use dungeon::{ Dungeon, - entity::{Entity, EntityKind, Item, PLAYER_INVENTORY_SIZE, Player}, + entity::{Entity, EntityKind, Item}, map::{Floor, MAP_SIZE, Tile}, + player::{PLAYER_INVENTORY_SIZE, Player}, pos::{Direction, Pos}, }; use raylib::{ @@ -635,21 +636,11 @@ impl Renderer { self.draw_text_vertical(r, b"INV", TEXT_X, UI_PADDING); // Draw slots - for idx in 0..PLAYER_INVENTORY_SIZE { - if idx >= PLAYER_INVENTORY_SIZE { - // This should never happen! - // Maybe use a different type?? - break; - } - - let slot_x = SLOTS_X + SLOT_LEN * idx; + for (idx, (opt_item, active)) in player.inventory.iter().enumerate() { + let slot_x = SLOTS_X + SLOT_LEN * downcast!(idx, u16); // Draw slot container - let tint = if (idx as usize) == player.active_inv_slot { - Color::YELLOW - } else { - Color::WHITE - }; + let tint = if active { Color::YELLOW } else { Color::WHITE }; r.draw_atlas( &self.textures.atlas, ATLAS_INV_CONTAINER, @@ -660,8 +651,8 @@ impl Renderer { tint, ); - if let Some(item) = player.inventory.get(idx as usize) { - let tex = self.textures.item_texture(*item); + if let Some(item) = opt_item { + let tex = self.textures.item_texture(item); const ITEM_PADDDING: u16 = UI_PADDING * 3; let dest_rec = rect! { slot_x + ITEM_PADDDING/2, -- cgit v1.2.3-freya