summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2025-11-20 18:23:20 -0500
committerFreya Murphy <freya@freyacat.org>2025-11-20 18:23:20 -0500
commit2d4644889dafa69c06e284b8b2cd18d2f65e57bc (patch)
treec26f19e9eedc845d3bfd86557880257af334f0cf
parentdungeon: make chest items optional (no item = open) (diff)
downloadDungeonCrawl-2d4644889dafa69c06e284b8b2cd18d2f65e57bc.tar.gz
DungeonCrawl-2d4644889dafa69c06e284b8b2cd18d2f65e57bc.tar.bz2
DungeonCrawl-2d4644889dafa69c06e284b8b2cd18d2f65e57bc.zip
dungeon: refactor player out of entity.rs
-rw-r--r--dungeon/src/entity.rs206
-rw-r--r--dungeon/src/lib.rs24
-rw-r--r--dungeon/src/player.rs294
-rw-r--r--game/src/lib.rs10
-rw-r--r--graphics/src/lib.rs3
-rw-r--r--graphics/src/render.rs23
6 files changed, 314 insertions, 246 deletions
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<Item>,
- pub weapon: Weapon,
- // How long until we reset potion effects?
- pub potion_timer: Option<Instant>,
- 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<Entity>,
- ) {
- 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<Entity>,
- ) {
- 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<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
+ }
+}
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<dyn std::error::Error>;
/// The `Result` type used witin this crate
pub type Result<T> = std::result::Result<T, crate::Error>;
-#[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,