From 3094a8af132837ba635f17184a36a1ed53564b61 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Thu, 19 Mar 2026 12:36:51 +0300 Subject: [PATCH] refactor: Non-breaking refactor - Everything should work as before - Added blank submodules in layout - Added blank submodules in player - Moved inventory::ui submodule to ui::inventory - Added blank submodules in ui::inventory - Re-exported ui::inventory as inventory::ui for temporary compatibility - Split tests submodules into different files --- src/inventory/ui.rs | 480 +----------------------------- src/layout/container.rs | 0 src/layout/door.rs | 0 src/{layout.rs => layout/mod.rs} | 4 + src/layout/systems.rs | 0 src/{player.rs => player/mod.rs} | 2 + src/player/systems.rs | 0 src/tests.rs | 393 ------------------------- src/tests/input.rs | 75 +++++ src/tests/inventory.rs | 313 ++++++++++++++++++++ src/tests/mod.rs | 2 + src/ui/inventory/mod.rs | 481 +++++++++++++++++++++++++++++++ src/ui/inventory/systems.rs | 0 src/{ui.rs => ui/mod.rs} | 2 + 14 files changed, 880 insertions(+), 872 deletions(-) create mode 100644 src/layout/container.rs create mode 100644 src/layout/door.rs rename src/{layout.rs => layout/mod.rs} (99%) create mode 100644 src/layout/systems.rs rename src/{player.rs => player/mod.rs} (99%) create mode 100644 src/player/systems.rs delete mode 100644 src/tests.rs create mode 100644 src/tests/input.rs create mode 100644 src/tests/inventory.rs create mode 100644 src/tests/mod.rs create mode 100644 src/ui/inventory/mod.rs create mode 100644 src/ui/inventory/systems.rs rename src/{ui.rs => ui/mod.rs} (98%) diff --git a/src/inventory/ui.rs b/src/inventory/ui.rs index bab72ca..acbd43c 100644 --- a/src/inventory/ui.rs +++ b/src/inventory/ui.rs @@ -1,479 +1 @@ -use std::f32::consts::FRAC_PI_2; - -use bevy::{ecs::relationship::RelatedSpawner, prelude::*, ui_widgets::{ControlOrientation, CoreScrollbarThumb, Scrollbar}}; - -use crate::{inventory::{ActiveInventory, Inventory, item::Item}, player::Player, ui::{UiRoot, UiRotateEvent}}; - -const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png"; -const TEMP_ITEM_PATH: &'static str = "sprites/items/choco_bar.png"; - -#[derive(Component, Reflect)] -#[require(Node)] -pub struct UiInventoryManager; - -#[derive(Component, Reflect)] -#[require(Node)] -pub struct UiInventory(pub Entity); - -#[derive(Component, Reflect)] -#[require(Node, ImageNode)] -pub struct UiInventorySlot(pub UVec2); - -#[derive(Component, Reflect)] -#[require(Node, ImageNode)] -pub struct UiItem(pub Entity); - -#[derive(Component, Reflect)] -pub struct HoveredItem; - -#[derive(Component, Reflect)] -pub struct HoveredSlot; - -#[derive(Component, Reflect)] -pub struct DraggedItem(pub Item, pub UVec2); - -fn ui_item_node_data(item: &Item) -> (Val, Val, Val, Val, UiTransform) { - match item.rotated { - true => ( - percent(50. * (item.size.x as f32 - item.size.y as f32)), - percent(50. * (item.size.y as f32 - item.size.x as f32)), - percent(100. * item.size.y as f32), - percent(100. * item.size.x as f32), - UiTransform::from_rotation(Rot2::radians(-FRAC_PI_2)), - ), - false => ( - auto(), - auto(), - percent(100. * item.size.x as f32), - percent(100. * item.size.y as f32), - UiTransform::default(), - ), - } -} - -fn update_ui_node(item: &Item, mut node: Mut<'_, Node>, mut ui_transform: Mut<'_, UiTransform>) { - let (left, top, min_width, min_height, new_ui_transform) = ui_item_node_data(item); - node.left = left; - node.top = top; - node.min_width = min_width; - node.min_height = min_height; - ui_transform.rotation = new_ui_transform.rotation; -} - -fn reset_slots_colors(query: Query<&mut ImageNode, With>) { - for mut image in query { - image.color = Color::WHITE; - } -} - -fn on_slot_over( - e: On>, - mut commands: Commands, - mut image_query: Query<&mut ImageNode, With>, - hovered_slots: Query>, - has_dragged_item: Option>>, -) { - if let Ok(mut image) = image_query.get_mut(e.event_target()) { - image.color = Color::WHITE.darker(0.3); - }; - for slot_id in hovered_slots { - commands.entity(slot_id).remove::(); - } - if has_dragged_item.is_none() { - commands.entity(e.event_target()).insert(HoveredSlot); - } -} - -fn on_slot_out(e: On>, mut query: Query<&mut ImageNode, With>) { - if let Ok(mut image) = query.get_mut(e.event_target()) { - image.color = Color::WHITE; - } -} - -fn on_item_over( - e: On>, - mut commands: Commands, - query: Query<(), With>, - has_hovered_item: Option>>, -) { - if has_hovered_item.is_some() { - return; - } - if let Ok(_) = query.get(e.event_target()) { - commands.entity(e.event_target()).insert(HoveredItem); - } -} - -fn on_item_out( - e: On>, - mut commands: Commands, - query: Query<(), (With, With)>, -) { - if let Ok(_) = query.get(e.event_target()) { - commands.entity(e.event_target()).remove::(); - } -} - -fn on_item_drag_start( - e: On>, - mut commands: Commands, - ui_query: Query<&UiItem, With>, - item_query: Query<&Item>, - hovered_slot: Single<&UiInventorySlot, With> -) { - if let Ok(ui_item) = ui_query.get(e.event_target()) { - let Ok(item) = item_query.get(ui_item.0) else { - error!("UiItem {} is pointing to non-existing Item", e.event_target()); - return; - }; - let Some(item_position) = item.position else { - return; - }; - let slot_position = hovered_slot.0; - let diff = slot_position - item_position; - commands.entity(e.event_target()).insert(DraggedItem(item.clone(), diff)); - } -} - -fn on_item_drag(e: On>, mut query: Query<&mut UiTransform, With>) { - if let Ok(mut transform) = query.get_mut(e.event_target()) { - transform.translation = Val2::px(e.distance.x, e.distance.y); - } -} - -fn on_item_drag_end( - e: On>, - mut commands: Commands, - mut query: Query<&mut UiTransform, With>, -) { - if let Ok(mut transform) = query.get_mut(e.event_target()) { - transform.translation = Val2::ZERO; - commands.entity(e.event_target()).remove::(); - } -} - -fn on_item_drag_drop( - event: On>, - mut commands: Commands, - mut ui_item_query: Query<(Entity, &UiItem, &mut DraggedItem, &mut Node, &mut UiTransform)>, - ui_inventory_query: Query<&UiInventory>, - mut item_query: Query<&mut Item>, - slot_query: Query<(&ChildOf, &UiInventorySlot, Option<&Children>)>, - slot_id_query: Query<(Entity, &UiInventorySlot, &ChildOf), With>, - inventory_query: Query<(&Inventory, Option<&Children>)>, -) { - let Ok((ui_item_entity, UiItem(item_entity), mut dragged_item, node, ui_transform)) = ui_item_query.get_mut(event.dropped) else { - return; - }; - let Ok((slot_parent, UiInventorySlot(new_position), slot_children)) = slot_query.get(event.event_target()) else { - return; - }; - if slot_children.is_some() { - return; - } - let Ok(UiInventory(inventory_id)) = ui_inventory_query.get(slot_parent.0) else { - return; - }; - let Ok((inventory, inventory_children)) = inventory_query.get(*inventory_id) else { - return; - }; - - let items = match inventory_children { - Some(children) => &children[..], - None => &[], - }; - - let actual_position = new_position.as_ivec2() - dragged_item.1.as_ivec2(); - info!("{actual_position:?}"); - let temp_item = &mut dragged_item.0; - if actual_position.is_negative_bitmask() == 0 { - temp_item.position = Some(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 { - return; - }; - - if inventory.can_replace(item_query.as_readonly(), items, *item_entity, temp_item) { - let mut item = item_query.get_mut(*item_entity).unwrap(); - item.position = temp_item.position; - item.size = temp_item.size; - item.rotated = temp_item.rotated; - commands.entity(ui_item_entity).insert(ChildOf(slot_id)); - commands.entity(*item_entity).insert(ChildOf(*inventory_id)); - commands.entity(slot_id).insert(HoveredSlot); - update_ui_node(item.as_ref(), node, ui_transform); - } else { - if let Ok(item) = item_query.get(*item_entity) { - update_ui_node(item, node, ui_transform); - } - } - commands.run_system_cached(reset_slots_colors); -} - -pub fn on_ui_rotate( - _: On, - mut item_query: Query<&mut Item>, - parent_query: Query<&ChildOf>, - inventory_query: Query<(&Inventory, Option<&Children>)>, - ui_item_query: Query<(&UiItem, &mut UiTransform, &mut Node, Option<&mut DraggedItem>), With>, -) { - for (ui_item, ui_transform, node, maybe_dragged) in ui_item_query { - let Ok(item_parent) = parent_query.get(ui_item.0) else { - continue; - }; - let Ok((inventory, children)) = inventory_query.get(item_parent.0) else { - continue; - }; - - let children = match children { - Some(children) => &children[..], - None => &[], - }; - let (was_rotated, item) = match maybe_dragged { - Some(mut temp_item) => { - temp_item.0.rotate(); - temp_item.1 = uvec2(temp_item.1.y, temp_item.1.x); - (true, Some(temp_item.0.clone())) - }, - None => { - let result = inventory.can_rotate(item_query.as_readonly(), children, ui_item.0); - let mut out_item = None; - if result { - if let Ok(mut item) = item_query.get_mut(ui_item.0) { - item.rotate(); - out_item = Some(item.clone()); - } - } - (result, out_item) - }, - }; - if was_rotated { - update_ui_node(&item.unwrap(), node, ui_transform); - } - } -} - -fn ui_manager_bundle(children: Vec, aligned_left: bool) -> impl Bundle { - let left = if aligned_left { Val::ZERO } else { percent(50.) }; - ( - UiInventoryManager, - Node { - position_type: PositionType::Absolute, - left, - width: percent(50.), - height: percent(100.), - scrollbar_width: 8., - display: Display::Grid, - grid_template_columns: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)], - grid_template_rows: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)], - ..default() - }, - Pickable::IGNORE, - GlobalZIndex::default(), - Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner| { - let scroll_area_id = parent.spawn(( - Node { - width: percent(100.), - height: percent(100.), - display: Display::Flex, - flex_direction: FlexDirection::Column, - align_items: AlignItems::Center, - justify_content: JustifyContent::SpaceAround, - overflow: Overflow::scroll_y(), - ..default() - }, - ScrollPosition(Vec2::ZERO), - )).add_children(children.as_slice()).id(); - parent.spawn(( - Node { - min_width: px(8), - grid_row: GridPlacement::start(1), - grid_column: GridPlacement::start(2), - ..default() - }, - Scrollbar { - orientation: ControlOrientation::Vertical, - target: scroll_area_id, - min_thumb_length: 8.0, - }, - BackgroundColor(Color::hsl(0., 0., 0.5)), - children![( - Node { - position_type: PositionType::Absolute, - border_radius: BorderRadius::all(px(4.)), - ..default() - }, - BackgroundColor(Color::hsl(0., 0., 0.3)), - CoreScrollbarThumb, - )], - )); - })), - ) -} - -fn ui_inventory_bundle( - inventory: &Inventory, - inventory_entity: Entity, -) -> impl Bundle { - ( - UiInventory(inventory_entity), - Node { - align_self: AlignSelf::Stretch, - align_content: AlignContent::Center, - display: Display::Grid, - width: percent(100.), - aspect_ratio: Some(inventory.size.x as f32 / inventory.size.y as f32), - grid_auto_columns: vec![GridTrack::percent(100. / inventory.size.x as f32)], - grid_auto_rows: vec![GridTrack::percent(100. / inventory.size.y as f32)], - ..default() - }, - Pickable::IGNORE, - GlobalZIndex::default(), - Name::new(format!("UiInventory ({}x{})", inventory.size.x, inventory.size.y)), - ) -} - -fn inventory_slot_bundle(x: u32, y: u32, image: Handle) -> impl Bundle { - ( - UiInventorySlot(UVec2::new(x, y)), - ImageNode { - color: Color::WHITE, - image, - image_mode: NodeImageMode::Stretch, - ..default() - }, - Node { - width: percent(100.), - height: percent(100.), - grid_column: GridPlacement::start(x as i16 + 1), - grid_row: GridPlacement::start(y as i16 + 1), - aspect_ratio: Some(1.), - ..default() - }, - Pickable { - should_block_lower: true, - is_hoverable: true, - }, - GlobalZIndex(1), - BackgroundColor::DEFAULT, - Name::new(format!("UiInventorySlot({x},{y})")), - ) -} - -fn ui_item_bundle(item: &Item, item_entity: Entity, image: Handle) -> impl Bundle { - let (left, top, min_width, min_height, ui_transform) = ui_item_node_data(item); - ( - UiItem(item_entity), - ImageNode { - image, - image_mode: NodeImageMode::Stretch, - ..default() - }, - Node { - left, - top, - min_width, - min_height, - ..default() - }, - BackgroundColor(Color::hsla(0., 0., 0., 0.5)), - ui_transform, - GlobalZIndex(2), - Pickable { - should_block_lower: false, - is_hoverable: true, - }, - Name::new(format!("UiItem ({},{})", - item.position.unwrap_or_default().x, - item.position.unwrap_or_default().y, - )), - ) -} - -pub fn setup_ui_inventory( - mut commands: Commands, - asset_server: Res, - inventory_query: Query<(Option<&ChildOf>, Entity, &Inventory, Option<&Children>)>, - player_query: Query<(), With>, - active_inventory_query: Query>, - item_query: Query<&Item>, - root_query: Query>, -) { - let Ok(root) = root_query.single() else { - error!("Query contains more than one UiRoot"); - return; - }; - let ui_slot_image: Handle = asset_server.load(UI_SLOT_ASSET_PATH); - let temp_item_image: Handle = asset_server.load(TEMP_ITEM_PATH); - 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::() { - let is_player = match inventory_parent { - Some(parent) => { - if player_query.get(parent.0) - .is_ok() { true } - else { - if active_inventory_query.get(inventory_entity) - .is_ok() { false } - else { continue; } - } - }, - None => { - if active_inventory_query.get(inventory_entity) - .is_ok() { false } - else { continue; } - }, - }; - let items = match children { - Some(children) => { - children.iter().filter_map(|item_entity| { - match item_query.get(item_entity) { - Ok(item) => Some((item, item_entity)), - Err(_) => None, - } - }).collect::>() - } - None => Vec::new(), - }; - let inventory_id = commands.spawn(ui_inventory_bundle(inventory, inventory_entity)) - .with_children(|commands| { - for x in 0..inventory.size.x { for y in 0..inventory.size.y { - let mut slot_commands = commands.spawn(inventory_slot_bundle(x, y, ui_slot_image.clone())); - slot_commands.observe(on_slot_over) - .observe(on_slot_out) - .observe(on_item_drag_drop); - if let Some((item, entity)) = items.iter() - .find(|(i, _)| i.position.unwrap_or_default() == UVec2::new(x, y)) { - slot_commands.with_children(|commands| { - commands.spawn(ui_item_bundle(item, *entity, temp_item_image.clone())) - .observe(on_item_over) - .observe(on_item_out) - .observe(on_item_drag_start) - .observe(on_item_drag) - .observe(on_item_drag_end); - }); - } - } } - }).id(); - if is_player { - player_inventory_ids.push(inventory_id); - } else { - active_inventory_ids.push(inventory_id); - } - } - let player_inventory_manager = commands.spawn(ui_manager_bundle(player_inventory_ids, true)).id(); - let active_inventory_manager = commands.spawn(ui_manager_bundle(active_inventory_ids, false)).id(); - for entity in active_inventory_query { - commands.entity(entity).remove::(); - } - commands.entity(root).add_children(&[player_inventory_manager, active_inventory_manager]); -} - -pub fn clear_ui_inventory( - mut commands: Commands, - inventory_query: Query>, -) { - for entity in inventory_query { - commands.entity(entity).despawn(); - } -} +pub use crate::ui::inventory::*; diff --git a/src/layout/container.rs b/src/layout/container.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/layout/door.rs b/src/layout/door.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/layout.rs b/src/layout/mod.rs similarity index 99% rename from src/layout.rs rename to src/layout/mod.rs index 9f021f9..9156229 100644 --- a/src/layout.rs +++ b/src/layout/mod.rs @@ -3,6 +3,10 @@ use bevy_rapier2d::prelude::*; use crate::{GameState, PIXELS_PER_METER, inventory::{ActiveInventory, Inventory, item::Item}, player::Player}; +pub mod container; +pub mod door; +pub mod systems; + const DOOR_OPENED_ASSET: &'static str = "sprites/interactive/door_opened.png"; const DOOR_CLOSED_ASSET: &'static str = "sprites/interactive/door_closed.png"; const CRATE_CLOSED_ASSET: &'static str = "sprites/interactive/crate_closed.png"; diff --git a/src/layout/systems.rs b/src/layout/systems.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/player.rs b/src/player/mod.rs similarity index 99% rename from src/player.rs rename to src/player/mod.rs index 0bd3f7c..8690b94 100644 --- a/src/player.rs +++ b/src/player/mod.rs @@ -4,6 +4,8 @@ use leafwing_input_manager::prelude::*; use crate::{GameState, InputAction as Action, PIXELS_PER_METER, inventory::{Inventory, item::Item}, layout::{InteractionEvent, MayInteract}}; +pub mod systems; + #[derive(Component, Reflect)] pub struct Player { // px/s diff --git a/src/player/systems.rs b/src/player/systems.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/tests.rs b/src/tests.rs deleted file mode 100644 index ad8978b..0000000 --- a/src/tests.rs +++ /dev/null @@ -1,393 +0,0 @@ -mod input { - 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, - } - - #[test] - fn asset_from_map() { - let mut input_map = InputMap::default(); - input_map.insert(Action::Button, KeyCode::KeyE); - input_map.insert(Action::Button, GamepadButton::East); - - input_map.insert_axis(Action::SingleAxis, GamepadAxis::LeftStickX); - 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); - } -} - -mod inventory { - use bevy::prelude::*; - use crate::inventory::{Inventory, item::Item, ui::DraggedItem}; - - fn inventory() -> Inventory { - Inventory::new(UVec2::splat(4)) - } - - /// 1x2 0;0 - fn item_a() -> Item { - Item::new_positioned(UVec2::new(1, 2), UVec2::new(0, 0)) - } - - /// 2x2 0;2 - fn item_b() -> Item { - Item::new_positioned(UVec2::new(2, 2), UVec2::new(0, 2)) - } - - /// 3x2 1;0 - fn item_c() -> Item { - Item::new_positioned(UVec2::new(3, 2), UVec2::new(1, 0)) - } - - /// 2x2 2;2 - fn item_d() -> Item { - Item::new_positioned(UVec2::new(2, 2), UVec2::new(2, 2)) - } - - /// 1x1 0;0 - fn item_e() -> Item { - Item::new_positioned(UVec2::new(1, 1), UVec2::new(0, 0)) - } - - /// 5x5 0;0 - fn item_f() -> Item { - Item::new_positioned(UVec2::new(5, 5), UVec2::new(0, 0)) - } - - /// 2x2 3;3 - fn item_g() -> Item { - Item::new_positioned(UVec2::new(2, 2), UVec2::new(3, 3)) - } - - #[derive(Resource)] - struct Items(Vec, usize, bool); - - #[derive(Component, Resource)] - struct MovableItem(UVec2, bool); - - #[derive(Component)] - struct RotatingMarker(bool); - - fn insert_item( - mut commands: Commands, - mut items: ResMut, - 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 { - if items.2 { - assert!(inventory.can_fit(item_query, children, q_size, q_pos)); - } else { - 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; - } - - fn try_to_move( - item_query: Query<&Item>, - inventory_query: Query<(&Inventory, &Children)>, - movable_item: Query<(Entity, &MovableItem)>, - ) { - let (inventory, children) = inventory_query.single().unwrap(); - let (movable_item, MovableItem(query_pos, assertion)) = movable_item.single().unwrap(); - if *assertion { - assert!(inventory.can_move(item_query, children, movable_item, *query_pos)); - } else { - assert!(!inventory.can_move(item_query, children, movable_item, *query_pos)); - } - } - - fn find_space( - item_query: Query<&Item>, - inventory_query: Query<(&Inventory, &Children)>, - movable_item: Res, - ) { - let (inventory, children) = inventory_query.single().unwrap(); - let MovableItem(query_size, assertion) = *movable_item; - if assertion { - assert!(inventory.find_free_space(item_query, children, query_size).is_some()); - } else { - assert!(inventory.find_free_space(item_query, children, query_size).is_none()); - } - } - - fn rotate_in_place( - item_query: Query<&Item>, - rotated_item: Query<(Entity, &RotatingMarker)>, - inventory_query: Query<(&Inventory, &Children)>, - ) { - let (inventory, children) = inventory_query.single().unwrap(); - let (rotatable_id, RotatingMarker(assertion)) = rotated_item.single().unwrap(); - if *assertion { - assert!(inventory.can_rotate(item_query, children, rotatable_id)); - } else { - assert!(!inventory.can_rotate(item_query, children, rotatable_id)); - } - } - - fn replace_item( - item_query: Query<&Item>, - replacable_item: Query<(Entity, &DraggedItem, &RotatingMarker)>, - inventory_query: Query<(&Inventory, &Children)>, - ) { - let (inventory, children) = inventory_query.single().unwrap(); - let (replacable_id, DraggedItem(replacing_item, _), RotatingMarker(assertion)) = replacable_item.single() - .unwrap(); - if *assertion { - assert!(inventory.can_replace(item_query, children, replacable_id, replacing_item)); - } else { - assert!(!inventory.can_replace(item_query, children, replacable_id, replacing_item)); - } - } - - #[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, true)); - - 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(insert_item); - - world.insert_resource(Items(vec![item_e(), item_f(), item_g()], 0, false)); - - 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"); - } - } - - #[test] - fn can_move_item() { - let mut world = World::new(); - - let system = world.register_system(try_to_move); - - world.spawn(inventory()) - .with_child((item_a(), MovableItem(UVec2::new(3, 2), true))) - .with_child(item_b()) - .with_child(item_c()); - - world.run_system(system).expect("Error on running system"); - } - - #[test] - fn cannot_move_item() { - let mut world = World::new(); - - let system = world.register_system(try_to_move); - - world.spawn(inventory()) - .with_child((item_a(), MovableItem(UVec2::new(3, 2), false))) - .with_child(item_b()) - .with_child(item_c()) - .with_child(item_d()); - - world.run_system(system).expect("Error on running system"); - } - - #[test] - fn move_item_slightly() { - let mut world = World::new(); - - let system = world.register_system(try_to_move); - - world.spawn(inventory()) - .with_child((item_a(), MovableItem(UVec2::new(0, 1), true))) - .with_child(item_c()) - .with_child(item_d()); - - world.run_system(system).expect("Error on running system"); - } - - #[test] - fn should_find_space_for_item() { - let mut world = World::new(); - - let system = world.register_system(find_space); - - world.insert_resource(MovableItem(UVec2::new(2, 2), true)); - - world.spawn(inventory()) - .with_child(item_a()) - .with_child(item_b()) - .with_child(item_c()); - - world.run_system(system).expect("Error on running system"); - } - - #[test] - fn should_not_find_space_for_item() { - let mut world = World::new(); - - let system = world.register_system(find_space); - - world.insert_resource(MovableItem(UVec2::new(2, 2), false)); - - world.spawn(inventory()) - .with_child(item_a()) - .with_child(item_b()) - .with_child(item_c()) - .with_child(item_d()); - - world.run_system(system).expect("Error on running system"); - } - - #[test] - fn item_fits_if_rotated() { - let mut world = World::new(); - - let system = world.register_system(find_space); - - world.insert_resource(MovableItem(UVec2::new(2, 3), true)); - - world.spawn(inventory()) - .with_child(item_a()) - .with_child(item_b()) - .with_child(item_d()); - - world.run_system(system).expect("Error on running system"); - } - - #[test] - fn rotate_item_in_place() { - let mut world = World::new(); - - let system = world.register_system(rotate_in_place); - - world.spawn(inventory()) - .with_child((item_a(), RotatingMarker(true))) - .with_child(item_b()) - .with_child(item_d()); - - world.run_system(system).expect("Error on running system"); - } - - #[test] - fn failed_rotate_item_in_place() { - let mut world = World::new(); - - let system = world.register_system(rotate_in_place); - - world.spawn(inventory()) - .with_child((item_a(), RotatingMarker(false))) - .with_child(item_b()) - .with_child(item_c()) - .with_child(item_d()); - - world.run_system(system).expect("Error on running system"); - } - - #[test] - fn can_replace_item() { - let mut world = World::new(); - - let system = world.register_system(replace_item); - - world.spawn(inventory()) - .with_child((item_a(), DraggedItem(item_c(), UVec2::ZERO), RotatingMarker(true))) - .with_child(item_b()) - .with_child(item_d()); - - world.run_system(system).expect("Error on running system"); - } - - #[test] - fn cannot_replace_item() { - let mut world = World::new(); - - let system = world.register_system(replace_item); - - world.spawn(inventory()) - .with_child((item_a(), DraggedItem(item_c(), UVec2::ZERO), RotatingMarker(false))) - .with_child(item_b()) - .with_child(item_c()) - .with_child(item_d()); - - world.run_system(system).expect("Error on running system"); - } -} diff --git a/src/tests/input.rs b/src/tests/input.rs new file mode 100644 index 0000000..ba932e9 --- /dev/null +++ b/src/tests/input.rs @@ -0,0 +1,75 @@ +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, +} + +#[test] +fn asset_from_map() { + let mut input_map = InputMap::default(); + input_map.insert(Action::Button, KeyCode::KeyE); + input_map.insert(Action::Button, GamepadButton::East); + + input_map.insert_axis(Action::SingleAxis, GamepadAxis::LeftStickX); + 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); +} diff --git a/src/tests/inventory.rs b/src/tests/inventory.rs new file mode 100644 index 0000000..6cd1ab3 --- /dev/null +++ b/src/tests/inventory.rs @@ -0,0 +1,313 @@ +use bevy::prelude::*; +use crate::inventory::{Inventory, item::Item, ui::DraggedItem}; + +fn inventory() -> Inventory { + Inventory::new(UVec2::splat(4)) +} + +/// 1x2 0;0 +fn item_a() -> Item { + Item::new_positioned(UVec2::new(1, 2), UVec2::new(0, 0)) +} + +/// 2x2 0;2 +fn item_b() -> Item { + Item::new_positioned(UVec2::new(2, 2), UVec2::new(0, 2)) +} + +/// 3x2 1;0 +fn item_c() -> Item { + Item::new_positioned(UVec2::new(3, 2), UVec2::new(1, 0)) +} + +/// 2x2 2;2 +fn item_d() -> Item { + Item::new_positioned(UVec2::new(2, 2), UVec2::new(2, 2)) +} + +/// 1x1 0;0 +fn item_e() -> Item { + Item::new_positioned(UVec2::new(1, 1), UVec2::new(0, 0)) +} + +/// 5x5 0;0 +fn item_f() -> Item { + Item::new_positioned(UVec2::new(5, 5), UVec2::new(0, 0)) +} + +/// 2x2 3;3 +fn item_g() -> Item { + Item::new_positioned(UVec2::new(2, 2), UVec2::new(3, 3)) +} + +#[derive(Resource)] +struct Items(Vec, usize, bool); + +#[derive(Component, Resource)] +struct MovableItem(UVec2, bool); + +#[derive(Component)] +struct RotatingMarker(bool); + +fn insert_item( + mut commands: Commands, + mut items: ResMut, + 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 { + if items.2 { + assert!(inventory.can_fit(item_query, children, q_size, q_pos)); + } else { + 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; +} + +fn try_to_move( + item_query: Query<&Item>, + inventory_query: Query<(&Inventory, &Children)>, + movable_item: Query<(Entity, &MovableItem)>, +) { + let (inventory, children) = inventory_query.single().unwrap(); + let (movable_item, MovableItem(query_pos, assertion)) = movable_item.single().unwrap(); + if *assertion { + assert!(inventory.can_move(item_query, children, movable_item, *query_pos)); + } else { + assert!(!inventory.can_move(item_query, children, movable_item, *query_pos)); + } +} + +fn find_space( + item_query: Query<&Item>, + inventory_query: Query<(&Inventory, &Children)>, + movable_item: Res, +) { + let (inventory, children) = inventory_query.single().unwrap(); + let MovableItem(query_size, assertion) = *movable_item; + if assertion { + assert!(inventory.find_free_space(item_query, children, query_size).is_some()); + } else { + assert!(inventory.find_free_space(item_query, children, query_size).is_none()); + } +} + +fn rotate_in_place( + item_query: Query<&Item>, + rotated_item: Query<(Entity, &RotatingMarker)>, + inventory_query: Query<(&Inventory, &Children)>, +) { + let (inventory, children) = inventory_query.single().unwrap(); + let (rotatable_id, RotatingMarker(assertion)) = rotated_item.single().unwrap(); + if *assertion { + assert!(inventory.can_rotate(item_query, children, rotatable_id)); + } else { + assert!(!inventory.can_rotate(item_query, children, rotatable_id)); + } +} + +fn replace_item( + item_query: Query<&Item>, + replacable_item: Query<(Entity, &DraggedItem, &RotatingMarker)>, + inventory_query: Query<(&Inventory, &Children)>, +) { + let (inventory, children) = inventory_query.single().unwrap(); + let (replacable_id, DraggedItem(replacing_item, _), RotatingMarker(assertion)) = replacable_item.single() + .unwrap(); + if *assertion { + assert!(inventory.can_replace(item_query, children, replacable_id, replacing_item)); + } else { + assert!(!inventory.can_replace(item_query, children, replacable_id, replacing_item)); + } +} + +#[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, true)); + + 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(insert_item); + + world.insert_resource(Items(vec![item_e(), item_f(), item_g()], 0, false)); + + 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"); + } +} + +#[test] +fn can_move_item() { + let mut world = World::new(); + + let system = world.register_system(try_to_move); + + world.spawn(inventory()) + .with_child((item_a(), MovableItem(UVec2::new(3, 2), true))) + .with_child(item_b()) + .with_child(item_c()); + + world.run_system(system).expect("Error on running system"); +} + +#[test] +fn cannot_move_item() { + let mut world = World::new(); + + let system = world.register_system(try_to_move); + + world.spawn(inventory()) + .with_child((item_a(), MovableItem(UVec2::new(3, 2), false))) + .with_child(item_b()) + .with_child(item_c()) + .with_child(item_d()); + + world.run_system(system).expect("Error on running system"); +} + +#[test] +fn move_item_slightly() { + let mut world = World::new(); + + let system = world.register_system(try_to_move); + + world.spawn(inventory()) + .with_child((item_a(), MovableItem(UVec2::new(0, 1), true))) + .with_child(item_c()) + .with_child(item_d()); + + world.run_system(system).expect("Error on running system"); +} + +#[test] +fn should_find_space_for_item() { + let mut world = World::new(); + + let system = world.register_system(find_space); + + world.insert_resource(MovableItem(UVec2::new(2, 2), true)); + + world.spawn(inventory()) + .with_child(item_a()) + .with_child(item_b()) + .with_child(item_c()); + + world.run_system(system).expect("Error on running system"); +} + +#[test] +fn should_not_find_space_for_item() { + let mut world = World::new(); + + let system = world.register_system(find_space); + + world.insert_resource(MovableItem(UVec2::new(2, 2), false)); + + world.spawn(inventory()) + .with_child(item_a()) + .with_child(item_b()) + .with_child(item_c()) + .with_child(item_d()); + + world.run_system(system).expect("Error on running system"); +} + +#[test] +fn item_fits_if_rotated() { + let mut world = World::new(); + + let system = world.register_system(find_space); + + world.insert_resource(MovableItem(UVec2::new(2, 3), true)); + + world.spawn(inventory()) + .with_child(item_a()) + .with_child(item_b()) + .with_child(item_d()); + + world.run_system(system).expect("Error on running system"); +} + +#[test] +fn rotate_item_in_place() { + let mut world = World::new(); + + let system = world.register_system(rotate_in_place); + + world.spawn(inventory()) + .with_child((item_a(), RotatingMarker(true))) + .with_child(item_b()) + .with_child(item_d()); + + world.run_system(system).expect("Error on running system"); +} + +#[test] +fn failed_rotate_item_in_place() { + let mut world = World::new(); + + let system = world.register_system(rotate_in_place); + + world.spawn(inventory()) + .with_child((item_a(), RotatingMarker(false))) + .with_child(item_b()) + .with_child(item_c()) + .with_child(item_d()); + + world.run_system(system).expect("Error on running system"); +} + +#[test] +fn can_replace_item() { + let mut world = World::new(); + + let system = world.register_system(replace_item); + + world.spawn(inventory()) + .with_child((item_a(), DraggedItem(item_c(), UVec2::ZERO), RotatingMarker(true))) + .with_child(item_b()) + .with_child(item_d()); + + world.run_system(system).expect("Error on running system"); +} + +#[test] +fn cannot_replace_item() { + let mut world = World::new(); + + let system = world.register_system(replace_item); + + world.spawn(inventory()) + .with_child((item_a(), DraggedItem(item_c(), UVec2::ZERO), RotatingMarker(false))) + .with_child(item_b()) + .with_child(item_c()) + .with_child(item_d()); + + world.run_system(system).expect("Error on running system"); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..416574d --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod input; +mod inventory; diff --git a/src/ui/inventory/mod.rs b/src/ui/inventory/mod.rs new file mode 100644 index 0000000..1df53b5 --- /dev/null +++ b/src/ui/inventory/mod.rs @@ -0,0 +1,481 @@ +use std::f32::consts::FRAC_PI_2; + +use bevy::{ecs::relationship::RelatedSpawner, prelude::*, ui_widgets::{ControlOrientation, CoreScrollbarThumb, Scrollbar}}; + +use crate::{inventory::{ActiveInventory, Inventory, item::Item}, player::Player, ui::{UiRoot, UiRotateEvent}}; + +pub mod systems; + +const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png"; +const TEMP_ITEM_PATH: &'static str = "sprites/items/choco_bar.png"; + +#[derive(Component, Reflect)] +#[require(Node)] +pub struct UiInventoryManager; + +#[derive(Component, Reflect)] +#[require(Node)] +pub struct UiInventory(pub Entity); + +#[derive(Component, Reflect)] +#[require(Node, ImageNode)] +pub struct UiInventorySlot(pub UVec2); + +#[derive(Component, Reflect)] +#[require(Node, ImageNode)] +pub struct UiItem(pub Entity); + +#[derive(Component, Reflect)] +pub struct HoveredItem; + +#[derive(Component, Reflect)] +pub struct HoveredSlot; + +#[derive(Component, Reflect)] +pub struct DraggedItem(pub Item, pub UVec2); + +fn ui_item_node_data(item: &Item) -> (Val, Val, Val, Val, UiTransform) { + match item.rotated { + true => ( + percent(50. * (item.size.x as f32 - item.size.y as f32)), + percent(50. * (item.size.y as f32 - item.size.x as f32)), + percent(100. * item.size.y as f32), + percent(100. * item.size.x as f32), + UiTransform::from_rotation(Rot2::radians(-FRAC_PI_2)), + ), + false => ( + auto(), + auto(), + percent(100. * item.size.x as f32), + percent(100. * item.size.y as f32), + UiTransform::default(), + ), + } +} + +fn update_ui_node(item: &Item, mut node: Mut<'_, Node>, mut ui_transform: Mut<'_, UiTransform>) { + let (left, top, min_width, min_height, new_ui_transform) = ui_item_node_data(item); + node.left = left; + node.top = top; + node.min_width = min_width; + node.min_height = min_height; + ui_transform.rotation = new_ui_transform.rotation; +} + +fn reset_slots_colors(query: Query<&mut ImageNode, With>) { + for mut image in query { + image.color = Color::WHITE; + } +} + +fn on_slot_over( + e: On>, + mut commands: Commands, + mut image_query: Query<&mut ImageNode, With>, + hovered_slots: Query>, + has_dragged_item: Option>>, +) { + if let Ok(mut image) = image_query.get_mut(e.event_target()) { + image.color = Color::WHITE.darker(0.3); + }; + for slot_id in hovered_slots { + commands.entity(slot_id).remove::(); + } + if has_dragged_item.is_none() { + commands.entity(e.event_target()).insert(HoveredSlot); + } +} + +fn on_slot_out(e: On>, mut query: Query<&mut ImageNode, With>) { + if let Ok(mut image) = query.get_mut(e.event_target()) { + image.color = Color::WHITE; + } +} + +fn on_item_over( + e: On>, + mut commands: Commands, + query: Query<(), With>, + has_hovered_item: Option>>, +) { + if has_hovered_item.is_some() { + return; + } + if let Ok(_) = query.get(e.event_target()) { + commands.entity(e.event_target()).insert(HoveredItem); + } +} + +fn on_item_out( + e: On>, + mut commands: Commands, + query: Query<(), (With, With)>, +) { + if let Ok(_) = query.get(e.event_target()) { + commands.entity(e.event_target()).remove::(); + } +} + +fn on_item_drag_start( + e: On>, + mut commands: Commands, + ui_query: Query<&UiItem, With>, + item_query: Query<&Item>, + hovered_slot: Single<&UiInventorySlot, With> +) { + if let Ok(ui_item) = ui_query.get(e.event_target()) { + let Ok(item) = item_query.get(ui_item.0) else { + error!("UiItem {} is pointing to non-existing Item", e.event_target()); + return; + }; + let Some(item_position) = item.position else { + return; + }; + let slot_position = hovered_slot.0; + let diff = slot_position - item_position; + commands.entity(e.event_target()).insert(DraggedItem(item.clone(), diff)); + } +} + +fn on_item_drag(e: On>, mut query: Query<&mut UiTransform, With>) { + if let Ok(mut transform) = query.get_mut(e.event_target()) { + transform.translation = Val2::px(e.distance.x, e.distance.y); + } +} + +fn on_item_drag_end( + e: On>, + mut commands: Commands, + mut query: Query<&mut UiTransform, With>, +) { + if let Ok(mut transform) = query.get_mut(e.event_target()) { + transform.translation = Val2::ZERO; + commands.entity(e.event_target()).remove::(); + } +} + +fn on_item_drag_drop( + event: On>, + mut commands: Commands, + mut ui_item_query: Query<(Entity, &UiItem, &mut DraggedItem, &mut Node, &mut UiTransform)>, + ui_inventory_query: Query<&UiInventory>, + mut item_query: Query<&mut Item>, + slot_query: Query<(&ChildOf, &UiInventorySlot, Option<&Children>)>, + slot_id_query: Query<(Entity, &UiInventorySlot, &ChildOf), With>, + inventory_query: Query<(&Inventory, Option<&Children>)>, +) { + let Ok((ui_item_entity, UiItem(item_entity), mut dragged_item, node, ui_transform)) = ui_item_query.get_mut(event.dropped) else { + return; + }; + let Ok((slot_parent, UiInventorySlot(new_position), slot_children)) = slot_query.get(event.event_target()) else { + return; + }; + if slot_children.is_some() { + return; + } + let Ok(UiInventory(inventory_id)) = ui_inventory_query.get(slot_parent.0) else { + return; + }; + let Ok((inventory, inventory_children)) = inventory_query.get(*inventory_id) else { + return; + }; + + let items = match inventory_children { + Some(children) => &children[..], + None => &[], + }; + + let actual_position = new_position.as_ivec2() - dragged_item.1.as_ivec2(); + info!("{actual_position:?}"); + let temp_item = &mut dragged_item.0; + if actual_position.is_negative_bitmask() == 0 { + temp_item.position = Some(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 { + return; + }; + + if inventory.can_replace(item_query.as_readonly(), items, *item_entity, temp_item) { + let mut item = item_query.get_mut(*item_entity).unwrap(); + item.position = temp_item.position; + item.size = temp_item.size; + item.rotated = temp_item.rotated; + commands.entity(ui_item_entity).insert(ChildOf(slot_id)); + commands.entity(*item_entity).insert(ChildOf(*inventory_id)); + commands.entity(slot_id).insert(HoveredSlot); + update_ui_node(item.as_ref(), node, ui_transform); + } else { + if let Ok(item) = item_query.get(*item_entity) { + update_ui_node(item, node, ui_transform); + } + } + commands.run_system_cached(reset_slots_colors); +} + +pub fn on_ui_rotate( + _: On, + mut item_query: Query<&mut Item>, + parent_query: Query<&ChildOf>, + inventory_query: Query<(&Inventory, Option<&Children>)>, + ui_item_query: Query<(&UiItem, &mut UiTransform, &mut Node, Option<&mut DraggedItem>), With>, +) { + for (ui_item, ui_transform, node, maybe_dragged) in ui_item_query { + let Ok(item_parent) = parent_query.get(ui_item.0) else { + continue; + }; + let Ok((inventory, children)) = inventory_query.get(item_parent.0) else { + continue; + }; + + let children = match children { + Some(children) => &children[..], + None => &[], + }; + let (was_rotated, item) = match maybe_dragged { + Some(mut temp_item) => { + temp_item.0.rotate(); + temp_item.1 = uvec2(temp_item.1.y, temp_item.1.x); + (true, Some(temp_item.0.clone())) + }, + None => { + let result = inventory.can_rotate(item_query.as_readonly(), children, ui_item.0); + let mut out_item = None; + if result { + if let Ok(mut item) = item_query.get_mut(ui_item.0) { + item.rotate(); + out_item = Some(item.clone()); + } + } + (result, out_item) + }, + }; + if was_rotated { + update_ui_node(&item.unwrap(), node, ui_transform); + } + } +} + +fn ui_manager_bundle(children: Vec, aligned_left: bool) -> impl Bundle { + let left = if aligned_left { Val::ZERO } else { percent(50.) }; + ( + UiInventoryManager, + Node { + position_type: PositionType::Absolute, + left, + width: percent(50.), + height: percent(100.), + scrollbar_width: 8., + display: Display::Grid, + grid_template_columns: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)], + grid_template_rows: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)], + ..default() + }, + Pickable::IGNORE, + GlobalZIndex::default(), + Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner| { + let scroll_area_id = parent.spawn(( + Node { + width: percent(100.), + height: percent(100.), + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + justify_content: JustifyContent::SpaceAround, + overflow: Overflow::scroll_y(), + ..default() + }, + ScrollPosition(Vec2::ZERO), + )).add_children(children.as_slice()).id(); + parent.spawn(( + Node { + min_width: px(8), + grid_row: GridPlacement::start(1), + grid_column: GridPlacement::start(2), + ..default() + }, + Scrollbar { + orientation: ControlOrientation::Vertical, + target: scroll_area_id, + min_thumb_length: 8.0, + }, + BackgroundColor(Color::hsl(0., 0., 0.5)), + children![( + Node { + position_type: PositionType::Absolute, + border_radius: BorderRadius::all(px(4.)), + ..default() + }, + BackgroundColor(Color::hsl(0., 0., 0.3)), + CoreScrollbarThumb, + )], + )); + })), + ) +} + +fn ui_inventory_bundle( + inventory: &Inventory, + inventory_entity: Entity, +) -> impl Bundle { + ( + UiInventory(inventory_entity), + Node { + align_self: AlignSelf::Stretch, + align_content: AlignContent::Center, + display: Display::Grid, + width: percent(100.), + aspect_ratio: Some(inventory.size.x as f32 / inventory.size.y as f32), + grid_auto_columns: vec![GridTrack::percent(100. / inventory.size.x as f32)], + grid_auto_rows: vec![GridTrack::percent(100. / inventory.size.y as f32)], + ..default() + }, + Pickable::IGNORE, + GlobalZIndex::default(), + Name::new(format!("UiInventory ({}x{})", inventory.size.x, inventory.size.y)), + ) +} + +fn inventory_slot_bundle(x: u32, y: u32, image: Handle) -> impl Bundle { + ( + UiInventorySlot(UVec2::new(x, y)), + ImageNode { + color: Color::WHITE, + image, + image_mode: NodeImageMode::Stretch, + ..default() + }, + Node { + width: percent(100.), + height: percent(100.), + grid_column: GridPlacement::start(x as i16 + 1), + grid_row: GridPlacement::start(y as i16 + 1), + aspect_ratio: Some(1.), + ..default() + }, + Pickable { + should_block_lower: true, + is_hoverable: true, + }, + GlobalZIndex(1), + BackgroundColor::DEFAULT, + Name::new(format!("UiInventorySlot({x},{y})")), + ) +} + +fn ui_item_bundle(item: &Item, item_entity: Entity, image: Handle) -> impl Bundle { + let (left, top, min_width, min_height, ui_transform) = ui_item_node_data(item); + ( + UiItem(item_entity), + ImageNode { + image, + image_mode: NodeImageMode::Stretch, + ..default() + }, + Node { + left, + top, + min_width, + min_height, + ..default() + }, + BackgroundColor(Color::hsla(0., 0., 0., 0.5)), + ui_transform, + GlobalZIndex(2), + Pickable { + should_block_lower: false, + is_hoverable: true, + }, + Name::new(format!("UiItem ({},{})", + item.position.unwrap_or_default().x, + item.position.unwrap_or_default().y, + )), + ) +} + +pub fn setup_ui_inventory( + mut commands: Commands, + asset_server: Res, + inventory_query: Query<(Option<&ChildOf>, Entity, &Inventory, Option<&Children>)>, + player_query: Query<(), With>, + active_inventory_query: Query>, + item_query: Query<&Item>, + root_query: Query>, +) { + let Ok(root) = root_query.single() else { + error!("Query contains more than one UiRoot"); + return; + }; + let ui_slot_image: Handle = asset_server.load(UI_SLOT_ASSET_PATH); + let temp_item_image: Handle = asset_server.load(TEMP_ITEM_PATH); + 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::() { + let is_player = match inventory_parent { + Some(parent) => { + if player_query.get(parent.0) + .is_ok() { true } + else { + if active_inventory_query.get(inventory_entity) + .is_ok() { false } + else { continue; } + } + }, + None => { + if active_inventory_query.get(inventory_entity) + .is_ok() { false } + else { continue; } + }, + }; + let items = match children { + Some(children) => { + children.iter().filter_map(|item_entity| { + match item_query.get(item_entity) { + Ok(item) => Some((item, item_entity)), + Err(_) => None, + } + }).collect::>() + } + None => Vec::new(), + }; + let inventory_id = commands.spawn(ui_inventory_bundle(inventory, inventory_entity)) + .with_children(|commands| { + for x in 0..inventory.size.x { for y in 0..inventory.size.y { + let mut slot_commands = commands.spawn(inventory_slot_bundle(x, y, ui_slot_image.clone())); + slot_commands.observe(on_slot_over) + .observe(on_slot_out) + .observe(on_item_drag_drop); + if let Some((item, entity)) = items.iter() + .find(|(i, _)| i.position.unwrap_or_default() == UVec2::new(x, y)) { + slot_commands.with_children(|commands| { + commands.spawn(ui_item_bundle(item, *entity, temp_item_image.clone())) + .observe(on_item_over) + .observe(on_item_out) + .observe(on_item_drag_start) + .observe(on_item_drag) + .observe(on_item_drag_end); + }); + } + } } + }).id(); + if is_player { + player_inventory_ids.push(inventory_id); + } else { + active_inventory_ids.push(inventory_id); + } + } + let player_inventory_manager = commands.spawn(ui_manager_bundle(player_inventory_ids, true)).id(); + let active_inventory_manager = commands.spawn(ui_manager_bundle(active_inventory_ids, false)).id(); + for entity in active_inventory_query { + commands.entity(entity).remove::(); + } + commands.entity(root).add_children(&[player_inventory_manager, active_inventory_manager]); +} + +pub fn clear_ui_inventory( + mut commands: Commands, + inventory_query: Query>, +) { + for entity in inventory_query { + commands.entity(entity).despawn(); + } +} diff --git a/src/ui/inventory/systems.rs b/src/ui/inventory/systems.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/ui.rs b/src/ui/mod.rs similarity index 98% rename from src/ui.rs rename to src/ui/mod.rs index 15c2fd8..0fea794 100644 --- a/src/ui.rs +++ b/src/ui/mod.rs @@ -3,6 +3,8 @@ use leafwing_input_manager::prelude::*; use crate::UiAction; +pub mod inventory; + #[derive(Component, Reflect)] #[require(Node)] pub struct UiRoot;