feat: Added Inventory and Item components

- Moved input tests into nested module
- Added inventory tests
This commit is contained in:
Alexey 2026-03-06 16:41:17 +03:00
commit dab3134f15
4 changed files with 260 additions and 72 deletions

35
src/inventory/item.rs Normal file
View file

@ -0,0 +1,35 @@
use bevy::prelude::*;
#[derive(Component, Clone)]
pub struct Item {
pub size: UVec2,
pub position: Option<UVec2>,
}
impl Item {
pub fn new(size: UVec2) -> Self {
Self { size, position: None }
}
pub fn new_positioned(size: UVec2, position: UVec2) -> Self {
Self { size, position: Some(position) }
}
pub fn rect(&self) -> Option<URect> {
let Some(position) = self.position else {
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()
}
}

39
src/inventory/mod.rs Normal file
View file

@ -0,0 +1,39 @@
use bevy::prelude::*;
pub mod item;
#[derive(Component)]
pub struct Inventory {
pub size: UVec2,
}
impl Inventory {
pub fn new(size: UVec2) -> Self {
Self { size }
}
pub fn can_fit(
&self,
item_query: Query<&item::Item>,
contained_items: &Children,
queried_size: UVec2,
queried_position: UVec2,
) -> bool {
let item_corner = queried_size + queried_position;
if item_corner.x > self.size.x || item_corner.y > self.size.y {
return false;
}
for entity in contained_items {
let Ok(item) = item_query.get(*entity) else {
warn!("Could not query inventory child ({entity}), probably not item?");
continue;
};
if item.overlaps(queried_size, queried_position) {
return false;
}
}
true
}
}

View file

@ -1,6 +1,7 @@
pub mod player; pub mod player;
pub mod layout; pub mod layout;
pub mod input; pub mod input;
pub mod inventory;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View file

@ -1,76 +1,189 @@
use super::*; mod input {
use leafwing_input_manager::prelude::*; use super::super::*;
use leafwing_input_manager::prelude::*;
#[derive(Actionlike, Reflect, Clone, Debug, PartialEq, Eq, Hash, Default)]
enum Action {
#[default]
#[actionlike(DualAxis)]
DualAxis,
#[actionlike(Axis)]
SingleAxis,
Button,
}
#[derive(Actionlike, Reflect, Clone, Debug, PartialEq, Eq, Hash, Default)] #[test]
enum Action { fn asset_from_map() {
#[default] let mut input_map = InputMap::default();
#[actionlike(DualAxis)] input_map.insert(Action::Button, KeyCode::KeyE);
DualAxis, input_map.insert(Action::Button, GamepadButton::East);
#[actionlike(Axis)]
SingleAxis, input_map.insert_axis(Action::SingleAxis, GamepadAxis::LeftStickX);
Button, input_map.insert_axis(Action::SingleAxis, VirtualAxis::ad());
input_map.insert_dual_axis(Action::DualAxis, GamepadStick::RIGHT);
input_map.insert_dual_axis(Action::DualAxis, VirtualDPad::wasd());
let mut expected_input_asset = input::InputAsset::default();
expected_input_asset.insert(Action::Button, input::MultiInput {
keyboard: Some(vec![KeyCode::KeyE]),
gamepad: Some(vec![GamepadButton::East]),
mouse: None,
}.into());
expected_input_asset.insert(Action::SingleAxis, input::InputKind::Axis(vec![
Box::new(GamepadAxis::LeftStickX),
Box::new(VirtualAxis::ad()),
]));
expected_input_asset.insert(Action::DualAxis, input::InputKind::DualAxis(vec![
Box::new(GamepadStick::RIGHT),
Box::new(VirtualDPad::wasd()),
]));
let input_asset = input::InputAsset::from(input_map);
assert_eq!(input_asset, expected_input_asset);
}
#[test]
fn map_from_asset() {
let mut input_asset = input::InputAsset::default();
input_asset.insert(Action::Button, input::MultiInput {
keyboard: Some(vec![KeyCode::KeyE]),
gamepad: Some(vec![GamepadButton::East]),
mouse: None,
}.into());
input_asset.insert(Action::SingleAxis, input::InputKind::Axis(vec![
Box::new(GamepadAxis::LeftStickX),
Box::new(VirtualAxis::ad()),
]));
input_asset.insert(Action::DualAxis, input::InputKind::DualAxis(vec![
Box::new(GamepadStick::RIGHT),
Box::new(VirtualDPad::wasd()),
]));
let mut expected_input_map = InputMap::default();
expected_input_map.insert(Action::Button, KeyCode::KeyE);
expected_input_map.insert(Action::Button, GamepadButton::East);
expected_input_map.insert_axis(Action::SingleAxis, GamepadAxis::LeftStickX);
expected_input_map.insert_axis(Action::SingleAxis, VirtualAxis::ad());
expected_input_map.insert_dual_axis(Action::DualAxis, GamepadStick::RIGHT);
expected_input_map.insert_dual_axis(Action::DualAxis, VirtualDPad::wasd());
let input_map = InputMap::from(input_asset);
assert_eq!(input_map, expected_input_map);
}
} }
#[test] mod inventory {
fn input_asset_from_map() { use bevy::prelude::*;
let mut input_map = InputMap::default(); use crate::inventory::{Inventory, item::Item};
input_map.insert(Action::Button, KeyCode::KeyE);
input_map.insert(Action::Button, GamepadButton::East);
input_map.insert_axis(Action::SingleAxis, GamepadAxis::LeftStickX); fn inventory() -> Inventory {
input_map.insert_axis(Action::SingleAxis, VirtualAxis::ad()); Inventory::new(UVec2::splat(4))
}
input_map.insert_dual_axis(Action::DualAxis, GamepadStick::RIGHT); fn item_a() -> Item {
input_map.insert_dual_axis(Action::DualAxis, VirtualDPad::wasd()); Item::new_positioned(UVec2::new(1, 2), UVec2::new(0, 0))
}
let mut expected_input_asset = input::InputAsset::default(); fn item_b() -> Item {
expected_input_asset.insert(Action::Button, input::MultiInput { Item::new_positioned(UVec2::new(2, 2), UVec2::new(0, 2))
keyboard: Some(vec![KeyCode::KeyE]), }
gamepad: Some(vec![GamepadButton::East]),
mouse: None,
}.into());
expected_input_asset.insert(Action::SingleAxis, input::InputKind::Axis(vec![
Box::new(GamepadAxis::LeftStickX),
Box::new(VirtualAxis::ad()),
]));
expected_input_asset.insert(Action::DualAxis, input::InputKind::DualAxis(vec![
Box::new(GamepadStick::RIGHT),
Box::new(VirtualDPad::wasd()),
]));
let input_asset = input::InputAsset::from(input_map); fn item_c() -> Item {
Item::new_positioned(UVec2::new(3, 2), UVec2::new(1, 0))
}
assert_eq!(input_asset, expected_input_asset); fn item_d() -> Item {
} Item::new_positioned(UVec2::new(2, 2), UVec2::new(2, 2))
}
#[test]
fn input_map_from_asset() { fn item_e() -> Item {
let mut input_asset = input::InputAsset::default(); Item::new_positioned(UVec2::new(1, 1), UVec2::new(0, 0))
input_asset.insert(Action::Button, input::MultiInput { }
keyboard: Some(vec![KeyCode::KeyE]),
gamepad: Some(vec![GamepadButton::East]), fn item_f() -> Item {
mouse: None, Item::new_positioned(UVec2::new(5, 5), UVec2::new(0, 0))
}.into()); }
input_asset.insert(Action::SingleAxis, input::InputKind::Axis(vec![
Box::new(GamepadAxis::LeftStickX), fn item_g() -> Item {
Box::new(VirtualAxis::ad()), Item::new_positioned(UVec2::new(2, 2), UVec2::new(3, 3))
])); }
input_asset.insert(Action::DualAxis, input::InputKind::DualAxis(vec![
Box::new(GamepadStick::RIGHT), #[derive(Resource)]
Box::new(VirtualDPad::wasd()), struct Items(Vec<Item>, usize);
]));
fn insert_item(
let mut expected_input_map = InputMap::default(); mut commands: Commands,
expected_input_map.insert(Action::Button, KeyCode::KeyE); mut items: ResMut<Items>,
expected_input_map.insert(Action::Button, GamepadButton::East); item_query: Query<&Item>,
inventory_query: Query<(Entity, &Inventory, Option<&Children>)>
expected_input_map.insert_axis(Action::SingleAxis, GamepadAxis::LeftStickX); ) {
expected_input_map.insert_axis(Action::SingleAxis, VirtualAxis::ad()); let item = &items.0[items.1];
let q_size = item.size;
expected_input_map.insert_dual_axis(Action::DualAxis, GamepadStick::RIGHT); let q_pos = item.position.unwrap();
expected_input_map.insert_dual_axis(Action::DualAxis, VirtualDPad::wasd()); let (entity, inventory, children) = inventory_query.single().unwrap();
if let Some(children) = children {
let input_map = InputMap::from(input_asset); println!("{q_size} {q_pos}");
assert!(inventory.can_fit(item_query, children, q_size, q_pos));
assert_eq!(input_map, expected_input_map); }
let item_entity = commands.spawn(item.clone()).id();
commands.entity(entity).add_child(item_entity);
items.1 += 1;
}
fn failed_insert_item(
mut commands: Commands,
mut items: ResMut<Items>,
item_query: Query<&Item>,
inventory_query: Query<(Entity, &Inventory, Option<&Children>)>
) {
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();
if let Some(children) = children {
println!("{q_size} {q_pos}");
assert!(!inventory.can_fit(item_query, children, q_size, q_pos));
}
let item_entity = commands.spawn(item.clone()).id();
commands.entity(entity).add_child(item_entity);
items.1 += 1;
}
#[test]
fn everything_fits() {
let mut world = World::new();
let system = world.register_system(insert_item);
world.insert_resource(Items(vec![item_a(), item_b(), item_c(), item_d()], 0));
world.spawn(inventory());
for _ in 0..4 {
world.run_system(system).expect("Error on running system");
}
}
#[test]
fn items_e_f_g_do_not_fit() {
let mut world = World::new();
let system = world.register_system(failed_insert_item);
world.insert_resource(Items(vec![item_e(), item_f(), item_g()], 0));
world.spawn(inventory())
.with_child(item_a())
.with_child(item_b())
.with_child(item_c())
.with_child(item_d());
for _ in 0..3 {
world.run_system(system).expect("Error on running system");
}
}
} }