From ab993be4768f93ed868f6ee6e180d0ab1b79178b Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Mon, 9 Mar 2026 13:59:51 +0300 Subject: [PATCH] ui: Beginning of UI-related stuff - UiRoot component - WindowSize resource and update_window_size system - UiInventory and UiInventorySlot components - UiInventory now shows player inventory slots --- Cargo.toml | 2 +- src/inventory/mod.rs | 5 +++ src/inventory/ui.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 18 ++++++++- src/player.rs | 36 +++++++++++++---- src/ui.rs | 39 +++++++++++++++++++ 6 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 src/inventory/ui.rs create mode 100644 src/ui.rs diff --git a/Cargo.toml b/Cargo.toml index 66fde66..6801f05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ version = "0.1.0" edition = "2024" [dependencies] -bevy = { version = "0.18.0" } +bevy = { version = "0.18.0", features = ["debug"] } bevy_common_assets = { version = "0.15.0", features = ["toml"] } bevy_input = { version = "0.18.0", features = ["serde", "serialize"] } leafwing-input-manager = "0.20.0" diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index d070aaf..eb9abca 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -1,12 +1,17 @@ use bevy::prelude::*; pub mod item; +pub mod ui; #[derive(Component)] pub struct Inventory { pub size: UVec2, } +/// Marker that this inventory will show up when UI is built +#[derive(Component)] +pub struct ActiveInventory; + impl Inventory { pub fn new(size: UVec2) -> Self { Self { size } diff --git a/src/inventory/ui.rs b/src/inventory/ui.rs new file mode 100644 index 0000000..61e27a5 --- /dev/null +++ b/src/inventory/ui.rs @@ -0,0 +1,93 @@ +use bevy::prelude::*; + +use crate::{inventory::{ActiveInventory, Inventory}, ui::{UiRoot, WindowSize}}; + +const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png"; + +#[derive(Component)] +#[require(Node)] +pub struct UiInventory; + +#[derive(Component)] +#[require(Node, ImageNode)] +pub struct UiInventorySlot(UVec2); + +fn ui_inventory_bundle(inventory: &Inventory, window_size: &Res) -> impl Bundle { + let window_ratio = window_size.aspect_ratio(); + let (width, height) = { + if window_ratio >= 1. { + (auto(), percent(100)) + } else { + (percent(100), auto()) + } + }; + ( + UiInventory, + Node { + align_self: AlignSelf::Center, + align_content: AlignContent::Center, + display: Display::Grid, + width, + height, + 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() + }, + ) +} + +fn inventory_slot_bundle(x: u32, y: u32, width: u32, height: u32, image: Handle) -> impl Bundle { + ( + UiInventorySlot(UVec2::new(x, y)), + ImageNode { + 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), + ..default() + }, + ) +} + +pub fn setup_ui_inventory( + mut commands: Commands, + asset_server: Res, + inventory_query: Query<(&Inventory, Option<&Children>), With>, + root_query: Query>, + window_size: Res, +) { + 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); + for (inventory, _children) in inventory_query { + let inventory_entity = commands.spawn(ui_inventory_bundle(inventory, &window_size)) + .with_children(|commands| { + for x in 0..inventory.size.x { + for y in 0..inventory.size.y { + commands.spawn(inventory_slot_bundle(x, y, inventory.size.x, inventory.size.y, ui_slot_image.clone())); + } + } + }).id(); + commands.entity(root).add_child(inventory_entity); + + // for simplicity we'll show only first inventory + break; + } +} + +pub fn clear_ui_inventory( + mut commands: Commands, + inventory_query: Query>, +) { + for entity in inventory_query { + commands.entity(entity).despawn(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1e74ec8..058853f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod player; pub mod layout; pub mod input; pub mod inventory; +pub mod ui; #[cfg(test)] mod tests; @@ -19,6 +20,13 @@ pub enum InputAction { Interact, } +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +pub enum GameState { + #[default] + Running, + Inventory, +} + impl InputAction { pub fn default_input_map() -> InputMap { let input_map = InputMap::default() @@ -51,12 +59,20 @@ fn camera_bundle() -> impl Bundle { fn setup_global(mut commands: Commands) { commands.spawn(camera_bundle()); + commands.spawn(ui::UiRoot::new()); } impl Plugin for ExpeditionPlugin { fn build(&self, app: &mut App) { app.add_plugins(input::InputAssetPlugin::::default()) + .init_state::() + .insert_resource(ui::WindowSize::default()) .add_systems(Startup, (player::setup_player, setup_global)) - .add_systems(Update, player::handle_input); + .add_systems(Update, ( + player::handle_input, + ui::update_window_size, + )) + .add_systems(OnEnter(GameState::Inventory), inventory::ui::setup_ui_inventory) + .add_systems(OnExit(GameState::Inventory), inventory::ui::clear_ui_inventory); } } diff --git a/src/player.rs b/src/player.rs index 724d6a6..902b2be 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,7 +1,7 @@ use bevy::prelude::*; use leafwing_input_manager::prelude::*; -use crate::InputAction as Action; +use crate::{GameState, InputAction as Action, inventory::{ActiveInventory, Inventory}}; #[derive(Component)] pub struct Player { @@ -18,6 +18,8 @@ fn player_bundle(asset_server: &Res) -> impl Bundle { Sprite::from_image(image), Transform::from_xyz(0f32, 0f32, 1f32), Action::default_input_map(), + Inventory::new(UVec2::new(12, 8)), + ActiveInventory, ) } @@ -27,15 +29,33 @@ pub fn setup_player(mut commands: Commands, asset_server: Res) { pub fn handle_input( time: Res