refactor!: Item changes

- Bump version to 0.3.0
- Moved crate::inventory::item to crate::item
- Made Item.position non-option
- Renamed Item::new_positioned -> Item::new
- Removed old Item::new method
- Changed Inventory.can_fit signature to use item borrow
This commit is contained in:
Alexey 2026-03-25 14:16:33 +03:00
commit 528c511445
13 changed files with 81 additions and 86 deletions

2
Cargo.lock generated
View file

@ -2422,7 +2422,7 @@ dependencies = [
[[package]] [[package]]
name = "expedition_demo" name = "expedition_demo"
version = "0.2.0" version = "0.3.0"
dependencies = [ dependencies = [
"bevy", "bevy",
"bevy_common_assets", "bevy_common_assets",

View file

@ -2,7 +2,7 @@ cargo-features = ["codegen-backend"]
[package] [package]
name = "expedition_demo" name = "expedition_demo"
version = "0.2.0" version = "0.3.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View file

@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
pub mod item; use crate::item::Item;
/// Marker that this inventory will show up when UI is built /// Marker that this inventory will show up when UI is built
#[derive(Component, Reflect, Default, Clone, Copy, Debug, PartialEq, Eq)] #[derive(Component, Reflect, Default, Clone, Copy, Debug, PartialEq, Eq)]
@ -20,12 +20,11 @@ impl Inventory {
pub fn can_fit( pub fn can_fit(
&self, &self,
item_query: Query<&item::Item>, item_query: Query<&Item>,
contained_items: &[Entity], contained_items: &[Entity],
queried_size: UVec2, queried_item: &Item,
queried_position: UVec2,
) -> bool { ) -> bool {
let item_corner = queried_size + queried_position; let item_corner = queried_item.size + queried_item.position;
if item_corner.x > self.size.x || item_corner.y > self.size.y { if item_corner.x > self.size.x || item_corner.y > self.size.y {
return false; return false;
} }
@ -35,7 +34,7 @@ impl Inventory {
continue; continue;
}; };
if item.overlaps(queried_size, queried_position) { if item.overlaps(queried_item) {
return false; return false;
} }
} }
@ -44,40 +43,37 @@ impl Inventory {
pub fn can_move( pub fn can_move(
&self, &self,
item_query: Query<&item::Item>, item_query: Query<&Item>,
contained_items: &[Entity], contained_items: &[Entity],
queried_item: Entity, queried_item: Entity,
queried_position: UVec2, queried_position: UVec2,
) -> bool { ) -> bool {
let Ok(item::Item {size, ..}) = item_query.get(queried_item) else { let Ok(item) = item_query.get(queried_item) else {
return false; return false;
}; };
let children = contained_items.iter() let children = contained_items.iter()
.filter_map(|e| if e.ne(&&queried_item) { Some(*e) } else { None }) .filter_map(|e| if e.ne(&&queried_item) { Some(*e) } else { None })
.collect::<Vec<Entity>>(); .collect::<Vec<Entity>>();
self.can_fit(item_query, children.as_slice(), *size, queried_position) let mock_item = Item::new(item.size, queried_position);
self.can_fit(item_query, children.as_slice(), &mock_item)
} }
pub fn can_replace( pub fn can_replace(
&self, &self,
item_query: Query<&item::Item>, item_query: Query<&Item>,
contained_items: &[Entity], contained_items: &[Entity],
replacable_item: Entity, replacable_item: Entity,
queried_item: &item::Item, queried_item: &Item,
) -> bool { ) -> bool {
let Some(position) = &queried_item.position else {
warn!("Trying to query rotated item without position");
return false;
};
let children = contained_items.iter() let children = contained_items.iter()
.filter_map(|e| if e.ne(&&replacable_item) { Some(*e) } else { None }) .filter_map(|e| if e.ne(&&replacable_item) { Some(*e) } else { None })
.collect::<Vec<Entity>>(); .collect::<Vec<Entity>>();
self.can_fit(item_query, children.as_slice(), queried_item.size, *position) self.can_fit(item_query, children.as_slice(), queried_item)
} }
pub fn can_rotate( pub fn can_rotate(
&self, &self,
item_query: Query<&item::Item>, item_query: Query<&Item>,
contained_items: &[Entity], contained_items: &[Entity],
queried_item: Entity, queried_item: Entity,
) -> bool { ) -> bool {
@ -85,20 +81,16 @@ impl Inventory {
error!("Could not query item"); error!("Could not query item");
return false; return false;
}; };
let Some(position) = &item.position else {
warn!("Trying to query rotated item without position");
return false;
};
let children = contained_items.iter() let children = contained_items.iter()
.filter_map(|e| if e.ne(&&queried_item) { Some(*e) } else { None }) .filter_map(|e| if e.ne(&&queried_item) { Some(*e) } else { None })
.collect::<Vec<Entity>>(); .collect::<Vec<Entity>>();
let rotated_item = item.clone_rotated(); let rotated_item = item.clone_rotated();
self.can_fit(item_query, children.as_slice(), rotated_item.size, *position) self.can_fit(item_query, children.as_slice(), &rotated_item)
} }
fn find_free_space_inner( fn find_free_space_inner(
&self, &self,
item_query: Query<&item::Item>, item_query: Query<&Item>,
contained_items: &[Entity], contained_items: &[Entity],
queried_size: UVec2, queried_size: UVec2,
was_swapped: bool, was_swapped: bool,
@ -108,9 +100,10 @@ impl Inventory {
}; };
for x in 0..=tries_x { for x in 0..=tries_x {
for y in 0..=tries_y { for y in 0..=tries_y {
let tested_pos = UVec2::new(x, y); let pos = UVec2::new(x, y);
if self.can_fit(item_query, contained_items, queried_size, tested_pos) { let mock_item = Item::new(queried_size, pos);
return Some((tested_pos, was_swapped)); if self.can_fit(item_query, contained_items, &mock_item) {
return Some((pos, was_swapped));
} }
} }
} }
@ -119,7 +112,7 @@ impl Inventory {
pub fn find_free_space( pub fn find_free_space(
&self, &self,
item_query: Query<&item::Item>, item_query: Query<&Item>,
contained_items: &[Entity], contained_items: &[Entity],
queried_size: UVec2, queried_size: UVec2,
) -> Option<(UVec2, bool)> { ) -> Option<(UVec2, bool)> {

View file

@ -13,7 +13,7 @@ pub struct Lockpick;
pub fn lockpick_bundle(asset_server: &Res<AssetServer>, position: UVec2) -> impl Bundle { pub fn lockpick_bundle(asset_server: &Res<AssetServer>, position: UVec2) -> impl Bundle {
let image = asset_server.load(LOCKPICK_SPRITE); let image = asset_server.load(LOCKPICK_SPRITE);
( (
Item::new_positioned(uvec2(1, 1), position), Item::new(uvec2(1, 1), position),
ItemImage(image), ItemImage(image),
Lockpick, Lockpick,
) )

View file

@ -6,7 +6,7 @@ pub mod lockpick;
#[derive(Component, Debug, Deref, DerefMut, PartialEq, Eq, Default, Clone, Reflect)] #[derive(Component, Debug, Deref, DerefMut, PartialEq, Eq, Default, Clone, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)] #[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct ItemImage(Handle<Image>); pub struct ItemImage(pub Handle<Image>);
// TODO: get rid of Option in position, it's no longer needed // TODO: get rid of Option in position, it's no longer needed
#[derive(Component, Clone, Debug, Reflect, PartialEq, Eq)] #[derive(Component, Clone, Debug, Reflect, PartialEq, Eq)]
@ -14,7 +14,7 @@ pub struct ItemImage(Handle<Image>);
#[require(ItemImage)] #[require(ItemImage)]
pub struct Item { pub struct Item {
pub size: UVec2, pub size: UVec2,
pub position: Option<UVec2>, pub position: UVec2,
pub rotated: bool, pub rotated: bool,
} }
@ -22,37 +22,24 @@ impl Default for Item {
fn default() -> Self { fn default() -> Self {
Self { Self {
size: uvec2(1, 1), size: uvec2(1, 1),
position: Some(uvec2(0, 0)), position: uvec2(0, 0),
rotated: false, rotated: false,
} }
} }
} }
impl Item { impl Item {
pub fn new(size: UVec2) -> Self { pub fn new(size: UVec2, position: UVec2) -> Self {
Self { size, position: None, rotated: false } Self { size, position, rotated: false }
} }
pub fn new_positioned(size: UVec2, position: UVec2) -> Self { #[inline(always)]
Self { size, position: Some(position), rotated: false } pub fn rect(&self) -> URect {
URect::from_corners(self.position, self.position + self.size)
} }
pub fn rect(&self) -> Option<URect> { pub fn overlaps(&self, other: &Item) -> bool {
let Some(position) = self.position else { !self.rect().intersect(other.rect()).is_empty()
return None;
};
Some(URect::from_corners(position, position + self.size))
}
pub fn overlaps(&self, other_size: UVec2, other_position: UVec2) -> bool {
let Some(rect) = self.rect() else {
return false;
};
let other_rect = URect::from_corners(other_position, other_position + other_size);
!rect.intersect(other_rect).is_empty()
} }
/// Swap size.x with size.y /// Swap size.x with size.y

View file

@ -1,7 +1,12 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use crate::{inventory::{Inventory, item::lockpick::Lockpick}, meters, player::Player}; use crate::{
meters,
inventory::Inventory,
item::lockpick::Lockpick,
player::Player,
};
use super::*; use super::*;

View file

@ -7,7 +7,7 @@ use bevy_rapier2d::prelude::*;
use crate::{ use crate::{
meters, meters,
inventory::item::lockpick::lockpick_bundle, item::lockpick::lockpick_bundle,
player::{ player::{
Player, Player,
player_bundle, player_bundle,

View file

@ -9,6 +9,7 @@ use bevy_rapier2d::{
pub mod input; pub mod input;
pub mod inventory; pub mod inventory;
pub mod item;
pub mod layout; pub mod layout;
pub mod player; pub mod player;
#[cfg(test)] #[cfg(test)]

View file

@ -2,8 +2,8 @@ use bevy::prelude::*;
use crate::{ use crate::{
inventory::{ inventory::{
Inventory, Inventory,
item::Item,
}, },
item::Item,
ui::inventory::DraggedItem, ui::inventory::DraggedItem,
}; };
@ -13,37 +13,37 @@ fn inventory() -> Inventory {
/// 1x2 0;0 /// 1x2 0;0
fn item_a() -> Item { fn item_a() -> Item {
Item::new_positioned(UVec2::new(1, 2), UVec2::new(0, 0)) Item::new(UVec2::new(1, 2), UVec2::new(0, 0))
} }
/// 2x2 0;2 /// 2x2 0;2
fn item_b() -> Item { fn item_b() -> Item {
Item::new_positioned(UVec2::new(2, 2), UVec2::new(0, 2)) Item::new(UVec2::new(2, 2), UVec2::new(0, 2))
} }
/// 3x2 1;0 /// 3x2 1;0
fn item_c() -> Item { fn item_c() -> Item {
Item::new_positioned(UVec2::new(3, 2), UVec2::new(1, 0)) Item::new(UVec2::new(3, 2), UVec2::new(1, 0))
} }
/// 2x2 2;2 /// 2x2 2;2
fn item_d() -> Item { fn item_d() -> Item {
Item::new_positioned(UVec2::new(2, 2), UVec2::new(2, 2)) Item::new(UVec2::new(2, 2), UVec2::new(2, 2))
} }
/// 1x1 0;0 /// 1x1 0;0
fn item_e() -> Item { fn item_e() -> Item {
Item::new_positioned(UVec2::new(1, 1), UVec2::new(0, 0)) Item::new(UVec2::new(1, 1), UVec2::new(0, 0))
} }
/// 5x5 0;0 /// 5x5 0;0
fn item_f() -> Item { fn item_f() -> Item {
Item::new_positioned(UVec2::new(5, 5), UVec2::new(0, 0)) Item::new(UVec2::new(5, 5), UVec2::new(0, 0))
} }
/// 2x2 3;3 /// 2x2 3;3
fn item_g() -> Item { fn item_g() -> Item {
Item::new_positioned(UVec2::new(2, 2), UVec2::new(3, 3)) Item::new(UVec2::new(2, 2), UVec2::new(3, 3))
} }
#[derive(Resource)] #[derive(Resource)]
@ -62,14 +62,12 @@ fn insert_item(
inventory_query: Query<(Entity, &Inventory, Option<&Children>)> inventory_query: Query<(Entity, &Inventory, Option<&Children>)>
) { ) {
let item = &items.0[items.1]; let item = &items.0[items.1];
let q_size = item.size;
let q_pos = item.position.unwrap();
let (entity, inventory, children) = inventory_query.single().unwrap(); let (entity, inventory, children) = inventory_query.single().unwrap();
if let Some(children) = children { if let Some(children) = children {
if items.2 { if items.2 {
assert!(inventory.can_fit(item_query, children, q_size, q_pos)); assert!(inventory.can_fit(item_query, children, item));
} else { } else {
assert!(!inventory.can_fit(item_query, children, q_size, q_pos)); assert!(!inventory.can_fit(item_query, children, item));
} }
} }
let item_entity = commands.spawn(item.clone()).id(); let item_entity = commands.spawn(item.clone()).id();

View file

@ -141,8 +141,8 @@ pub fn ui_item_bundle(item: &Item, item_entity: Entity, image: Handle<Image>) ->
is_hoverable: true, is_hoverable: true,
}, },
Name::new(format!("UiItem ({},{})", Name::new(format!("UiItem ({},{})",
item.position.unwrap_or_default().x, item.position.x,
item.position.unwrap_or_default().y, item.position.y,
)), )),
) )
} }

View file

@ -5,8 +5,8 @@ use bevy::prelude::*;
use crate::{ use crate::{
inventory::{ inventory::{
Inventory, Inventory,
item::Item,
}, },
item::Item,
ui::UiRotateEvent, ui::UiRotateEvent,
}; };

View file

@ -62,11 +62,8 @@ pub fn on_item_drag_start(
error!("UiItem {} is pointing to non-existing Item", e.event_target()); error!("UiItem {} is pointing to non-existing Item", e.event_target());
return; return;
}; };
let Some(item_position) = item.position else {
return;
};
let slot_position = hovered_slot.0; let slot_position = hovered_slot.0;
let diff = slot_position - item_position; let diff = slot_position - item.position;
commands.entity(e.event_target()).insert(DraggedItem(item.clone(), diff)); commands.entity(e.event_target()).insert(DraggedItem(item.clone(), diff));
} }
} }
@ -123,15 +120,18 @@ pub fn on_item_drag_drop(
info!("{actual_position:?}"); info!("{actual_position:?}");
let temp_item = &mut dragged_item.0; let temp_item = &mut dragged_item.0;
if actual_position.is_negative_bitmask() == 0 { if actual_position.is_negative_bitmask() == 0 {
temp_item.position = Some(actual_position.as_uvec2()); temp_item.position = actual_position.as_uvec2();
} }
let Some((slot_id, _, _)) = slot_id_query.iter().find(|(_, slot_pos, slot_parent_id)| slot_pos.0 == temp_item.position.unwrap() && slot_parent_id == &slot_parent) else { let Some((slot_id, _, _)) = slot_id_query.iter().find(|(_, slot_pos, slot_parent_id)| slot_pos.0 == temp_item.position && slot_parent_id == &slot_parent) else {
return; return;
}; };
if inventory.can_replace(item_query.as_readonly(), items, *item_entity, temp_item) { if inventory.can_replace(item_query.as_readonly(), items, *item_entity, temp_item) {
let mut item = item_query.get_mut(*item_entity).unwrap(); let Ok(mut item) = item_query.get_mut(*item_entity) else {
error!("Cannot get item to replace");
return;
};
item.position = temp_item.position; item.position = temp_item.position;
item.size = temp_item.size; item.size = temp_item.size;
item.rotated = temp_item.rotated; item.rotated = temp_item.rotated;

View file

@ -2,14 +2,21 @@ use bevy::prelude::*;
use crate::{ use crate::{
inventory::ActiveInventory, inventory::ActiveInventory,
item::{
Item,
ItemImage,
},
player::Player, player::Player,
ui::UiRoot, ui::UiRoot,
}; };
use super::{*, bundles::*, observers::*}; use super::{
*,
bundles::*,
observers::*,
};
const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png"; const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png";
const TEMP_ITEM_PATH: &'static str = "sprites/items/choco_bar.png";
pub fn setup_ui_inventory( pub fn setup_ui_inventory(
mut commands: Commands, mut commands: Commands,
@ -18,6 +25,7 @@ pub fn setup_ui_inventory(
player_query: Query<(), With<Player>>, player_query: Query<(), With<Player>>,
active_inventory_query: Query<Entity, With<ActiveInventory>>, active_inventory_query: Query<Entity, With<ActiveInventory>>,
item_query: Query<&Item>, item_query: Query<&Item>,
item_image_query: Query<&ItemImage>,
root_query: Query<Entity, With<UiRoot>>, root_query: Query<Entity, With<UiRoot>>,
) { ) {
let Ok(root) = root_query.single() else { let Ok(root) = root_query.single() else {
@ -25,7 +33,6 @@ pub fn setup_ui_inventory(
return; return;
}; };
let ui_slot_image: Handle<Image> = asset_server.load(UI_SLOT_ASSET_PATH); let ui_slot_image: Handle<Image> = asset_server.load(UI_SLOT_ASSET_PATH);
let temp_item_image: Handle<Image> = asset_server.load(TEMP_ITEM_PATH);
let (mut player_inventory_ids, mut active_inventory_ids) = (Vec::new(), Vec::new()); let (mut player_inventory_ids, mut active_inventory_ids) = (Vec::new(), Vec::new());
for (inventory_parent, inventory_entity, inventory, children) in inventory_query.iter().sort::<Entity>() { for (inventory_parent, inventory_entity, inventory, children) in inventory_query.iter().sort::<Entity>() {
let is_player = match inventory_parent { let is_player = match inventory_parent {
@ -48,10 +55,14 @@ pub fn setup_ui_inventory(
Some(children) => { Some(children) => {
children.iter().filter_map(|item_entity| { children.iter().filter_map(|item_entity| {
match item_query.get(item_entity) { match item_query.get(item_entity) {
Ok(item) => Some((item, item_entity)), Ok(item) => {
let item_image = item_image_query.get(item_entity)
.expect("ItemImage is required on Item");
Some((item, item_entity, item_image))
},
Err(_) => None, Err(_) => None,
} }
}).collect::<Vec<(&Item, Entity)>>() }).collect::<Vec<(&Item, Entity, &ItemImage)>>()
} }
None => Vec::new(), None => Vec::new(),
}; };
@ -62,10 +73,10 @@ pub fn setup_ui_inventory(
slot_commands.observe(on_slot_over) slot_commands.observe(on_slot_over)
.observe(on_slot_out) .observe(on_slot_out)
.observe(on_item_drag_drop); .observe(on_item_drag_drop);
if let Some((item, entity)) = items.iter() if let Some((item, entity, item_image)) = items.iter()
.find(|(i, _)| i.position.unwrap_or_default() == UVec2::new(x, y)) { .find(|(i, _, _)| i.position == UVec2::new(x, y)) {
slot_commands.with_children(|commands| { slot_commands.with_children(|commands| {
commands.spawn(ui_item_bundle(item, *entity, temp_item_image.clone())) commands.spawn(ui_item_bundle(item, *entity, item_image.0.clone()))
.observe(on_item_over) .observe(on_item_over)
.observe(on_item_out) .observe(on_item_out)
.observe(on_item_drag_start) .observe(on_item_drag_start)