feat: Inventory mouse scroll & visible item data

This commit is contained in:
Alexey 2026-03-26 15:12:13 +03:00
commit 0e18bb6df5
7 changed files with 119 additions and 18 deletions

View file

@ -2,7 +2,6 @@ use bevy::prelude::*;
use super::*; use super::*;
// TODO: replace with proper sprite
const LOCKPICK_SPRITE: &'static str = "sprites/items/lockpick.png"; const LOCKPICK_SPRITE: &'static str = "sprites/items/lockpick.png";
#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
@ -14,7 +13,11 @@ pub fn lockpick_bundle(asset_server: &Res<AssetServer>, position: UVec2) -> impl
let image = asset_server.load(LOCKPICK_SPRITE); let image = asset_server.load(LOCKPICK_SPRITE);
( (
Item::new(uvec2(1, 1), position), Item::new(uvec2(1, 1), position),
ItemImage(image), ItemData {
image,
name: "Lockpick".into(),
description: "Consumable item used to picking locked doors".into(),
},
Lockpick, Lockpick,
) )
} }

View file

@ -4,14 +4,17 @@ use bevy::prelude::*;
pub mod lockpick; pub mod lockpick;
#[derive(Component, Debug, Deref, DerefMut, PartialEq, Eq, Default, Clone, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)] #[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct ItemImage(pub Handle<Image>); pub struct ItemData {
pub image: Handle<Image>,
pub name: String,
pub description: String,
}
// 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)]
#[reflect(Component, Clone, Debug, PartialEq)] #[reflect(Component, Clone, Debug, PartialEq)]
#[require(ItemImage)] #[require(ItemData)]
pub struct Item { pub struct Item {
pub size: UVec2, pub size: UVec2,
pub position: UVec2, pub position: UVec2,

View file

@ -153,5 +153,5 @@ pub fn setup_world(
uvec2(1, 1), uvec2(1, 1),
)).with_child(lockpick_bundle(&asset_server, UVec2::ZERO)); )).with_child(lockpick_bundle(&asset_server, UVec2::ZERO));
commands.spawn(player_bundle(&asset_server, vec2(meters(-6.), 0.))); commands.spawn(player_bundle(&asset_server, vec2(meters(-6.), meters(4.))));
} }

View file

@ -8,6 +8,8 @@ use bevy::{
}, },
}; };
use crate::item::ItemData;
use super::*; use super::*;
pub fn ui_manager_bundle(children: Vec<Entity>, aligned_left: bool) -> impl Bundle { pub fn ui_manager_bundle(children: Vec<Entity>, aligned_left: bool) -> impl Bundle {
@ -29,6 +31,7 @@ pub fn ui_manager_bundle(children: Vec<Entity>, aligned_left: bool) -> impl Bund
GlobalZIndex::default(), GlobalZIndex::default(),
Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<ChildOf>| { Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<ChildOf>| {
let scroll_area_id = parent.spawn(( let scroll_area_id = parent.spawn((
InventoryScrollArea,
Node { Node {
width: percent(100.), width: percent(100.),
height: percent(100.), height: percent(100.),
@ -40,7 +43,14 @@ pub fn ui_manager_bundle(children: Vec<Entity>, aligned_left: bool) -> impl Bund
..default() ..default()
}, },
ScrollPosition(Vec2::ZERO), ScrollPosition(Vec2::ZERO),
)).add_children(children.as_slice()).id(); Pickable {
is_hoverable: true,
should_block_lower: false,
}
))
.add_children(children.as_slice())
.observe(observers::on_scroll)
.id();
parent.spawn(( parent.spawn((
Node { Node {
min_width: px(8), min_width: px(8),
@ -146,3 +156,46 @@ pub fn ui_item_bundle(item: &Item, item_entity: Entity, image: Handle<Image>) ->
)), )),
) )
} }
pub fn hovered_item_data_bundle(item_data: &ItemData) -> impl Bundle {
(
HoveredItemData,
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
min_width: percent(100.),
top: percent(100.),
align_self: AlignSelf::Start,
justify_self: JustifySelf::Center,
overflow: Overflow::visible(),
border: UiRect::all(px(1.)),
..default()
},
BackgroundColor(Color::hsla(0., 0., 0.3, 0.8)),
BorderColor::all(Color::BLACK),
Pickable::IGNORE,
Name::new("HoveredItemData"),
children![
(
Text::new(item_data.name.clone()),
TextFont {
font_size: 24.,
..default()
},
TextLayout::new_with_justify(Justify::Center),
TextColor::WHITE,
Pickable::IGNORE,
),
(
Text::new(item_data.description.clone()),
TextFont {
font_size: 14.,
..default()
},
TextLayout::new(Justify::Justified, LineBreak::WordBoundary),
TextColor(Color::hsl(0., 0., 0.8)),
Pickable::IGNORE,
),
],
)
}

View file

@ -19,6 +19,15 @@ pub mod systems;
#[require(Node)] #[require(Node)]
pub struct UiInventoryManager; pub struct UiInventoryManager;
#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
#[require(Node)]
pub struct InventoryScrollArea;
#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct HoveredScrollArea;
#[derive(Component, Debug, PartialEq, Eq, Clone, Copy, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)] #[reflect(Component, Debug, PartialEq, Clone)]
#[require(Node)] #[require(Node)]
@ -38,6 +47,11 @@ pub struct UiItem(pub Entity);
#[reflect(Component, Debug, PartialEq, Default, Clone)] #[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct HoveredItem; pub struct HoveredItem;
#[derive(Component, Debug, PartialEq, Eq, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
#[require(Node)]
pub struct HoveredItemData;
#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)] #[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct HoveredSlot; pub struct HoveredSlot;

View file

@ -1,7 +1,28 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy_input::mouse::MouseScrollUnit;
use crate::item::ItemData;
use super::*; use super::*;
pub fn on_scroll(
e: On<Pointer<Scroll>>,
mut q: Query<&mut ScrollPosition, With<InventoryScrollArea>>,
) {
let Ok(mut scroll_position) = q.get_mut(e.event_target()) else {
return;
};
let multiplier = match e.unit {
MouseScrollUnit::Line => {
MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR
},
MouseScrollUnit::Pixel => 1.,
};
scroll_position.0.y -= e.y * multiplier;
}
pub fn on_slot_over( pub fn on_slot_over(
e: On<Pointer<Over>>, e: On<Pointer<Over>>,
mut commands: Commands, mut commands: Commands,
@ -29,14 +50,20 @@ pub fn on_slot_out(e: On<Pointer<Out>>, mut query: Query<&mut ImageNode, With<Ui
pub fn on_item_over( pub fn on_item_over(
e: On<Pointer<Over>>, e: On<Pointer<Over>>,
mut commands: Commands, mut commands: Commands,
query: Query<(), With<UiItem>>, ui_item_query: Query<&UiItem>,
item_data_query: Query<&ItemData>,
has_hovered_item: Option<Single<(), With<HoveredItem>>>, has_hovered_item: Option<Single<(), With<HoveredItem>>>,
) { ) {
if has_hovered_item.is_some() { if has_hovered_item.is_some() {
return; return;
} }
if let Ok(_) = query.get(e.event_target()) { if let Ok(UiItem(item_id)) = ui_item_query.get(e.event_target()) {
commands.entity(e.event_target()).insert(HoveredItem); let Ok(item_data) = item_data_query.get(*item_id) else {
error!("UiItem {} is pointing to non-existing Item", e.event_target());
return;
};
commands.entity(e.event_target()).insert(HoveredItem)
.with_child(bundles::hovered_item_data_bundle(item_data));
} }
} }
@ -46,7 +73,8 @@ pub fn on_item_out(
query: Query<(), (With<UiItem>, With<HoveredItem>)>, query: Query<(), (With<UiItem>, With<HoveredItem>)>,
) { ) {
if let Ok(_) = query.get(e.event_target()) { if let Ok(_) = query.get(e.event_target()) {
commands.entity(e.event_target()).remove::<HoveredItem>(); commands.entity(e.event_target()).remove::<HoveredItem>()
.despawn_children();
} }
} }

View file

@ -4,7 +4,7 @@ use crate::{
inventory::ActiveInventory, inventory::ActiveInventory,
item::{ item::{
Item, Item,
ItemImage, ItemData,
}, },
player::Player, player::Player,
ui::UiRoot, ui::UiRoot,
@ -25,7 +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>, item_image_query: Query<&ItemData>,
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 {
@ -57,12 +57,12 @@ pub fn setup_ui_inventory(
match item_query.get(item_entity) { match item_query.get(item_entity) {
Ok(item) => { Ok(item) => {
let item_image = item_image_query.get(item_entity) let item_image = item_image_query.get(item_entity)
.expect("ItemImage is required on Item"); .expect("ItemData is required on Item");
Some((item, item_entity, item_image)) Some((item, item_entity, &item_image.image))
}, },
Err(_) => None, Err(_) => None,
} }
}).collect::<Vec<(&Item, Entity, &ItemImage)>>() }).collect::<Vec<(&Item, Entity, &Handle<Image>)>>()
} }
None => Vec::new(), None => Vec::new(),
}; };
@ -76,7 +76,7 @@ pub fn setup_ui_inventory(
if let Some((item, entity, item_image)) = items.iter() if let Some((item, entity, item_image)) = items.iter()
.find(|(i, _, _)| i.position == 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, item_image.0.clone())) commands.spawn(ui_item_bundle(item, *entity, (*item_image).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)