use bevy::{log, prelude::*}; use bevy_tileset::prelude::{TileIndex, Tilesets}; use crate::core::animation::SimpleAnimation; use crate::playing::input::ControlledByPlayer; use crate::playing::inventory::{InventoryItem, ItemType, NoonmywaInventory}; use crate::playing::GameMode; use crate::{despawn_with, GameState}; use super::text_bundle; //use bevy_mod_picking::prelude::*; pub struct InventoryUiPlugin; #[derive(Resource, Default, PartialEq)] pub enum InventoryToggle { Active, #[default] Inactive, } impl InventoryToggle { pub fn toggle(&self) -> Self { match self { InventoryToggle::Active => InventoryToggle::Inactive, InventoryToggle::Inactive => InventoryToggle::Active, } } } impl Plugin for InventoryUiPlugin { fn build(&self, app: &mut App) { app.init_resource::().add_systems( Update, ( initialize_player_inventory_with_tileset .run_if(resource_exists_and_equals(GameMode::Creative)), show_inventory.run_if( resource_exists_and_changed::() .and_then(resource_equals(InventoryToggle::Active)), ), interaction.run_if(resource_equals(InventoryToggle::Active)), select_item.run_if(resource_equals(InventoryToggle::Active)), despawn_with::.run_if( resource_exists_and_changed::() .and_then(resource_equals(InventoryToggle::Inactive)), ), ) .run_if(in_state(GameState::Playing)), ); } } #[derive(Component)] struct UiInventory; #[derive(Component)] struct UiInventoryItem { pub slot: u8, } mod colors { use bevy::prelude::Color; pub const BUTTON_NORMAL: Color = Color::rgb(0.15, 0.15, 0.15); pub const BUTTON_HOVERED: Color = Color::rgb(0.35, 0.75, 0.35); pub const BUTTON_PRESSED: Color = Color::rgb(0.35, 0.75, 0.35); } fn interaction( mut q: Query< (&Interaction, &mut BackgroundColor, &mut Style), (Changed, With), >, ) { for (interaction, mut color, _style) in &mut q { match *interaction { Interaction::Pressed => { *color = colors::BUTTON_PRESSED.into(); //style.left = style.left.try_sub(Val::Percent(1.0)).unwrap(); } Interaction::Hovered => *color = colors::BUTTON_HOVERED.into(), Interaction::None => *color = colors::BUTTON_NORMAL.into(), } } } fn select_item( mut q: Query< ( &Interaction, &mut BackgroundColor, &mut Style, &UiInventoryItem, ), Changed, >, mut q_inventory: Query<&mut NoonmywaInventory, With>, ) { let Ok(mut inventory) = q_inventory.get_single_mut() else { log::warn!("No ControlledByPlayer with inventory found"); return }; for (interaction, mut color, mut style, item) in &mut q { match *interaction { Interaction::Pressed => { *color = colors::BUTTON_PRESSED.into(); inventory.set_active_slot(item.slot); } Interaction::Hovered => { *color = colors::BUTTON_HOVERED.into(); } Interaction::None => { *color = colors::BUTTON_NORMAL.into(); style.border = UiRect::all(Val::Px(10.0)); } } } } fn initialize_player_inventory_with_tileset( mut cmd: Commands, mut q_inventory: Query<&mut NoonmywaInventory, With>, mut is_initialized: Local, tilesets: Tilesets, ) { if *is_initialized { return; } let Some(tileset) = tilesets.get_by_name("Noonmywa Tileset") else { log::info!("Tileset doesn't exist yet!"); return }; let Ok(mut inventory) = q_inventory.get_single_mut() else { log::warn!("No ControlledByPlayer with inventory found"); return }; // FIXME as we cannot get names of all tiles in the tileset, let's just brute-force a bit for (_tile_group_id, tile_name) in tileset.get_tile_names() { let entity = cmd .spawn(( ItemType::Tile(tile_name.clone()), Name::from(format!("Tile@{tile_name}")), )) .id(); let item = InventoryItem { name: tile_name.clone(), entity, }; if let Err(e) = inventory.offer(item) { log::error!("Failed to offer tile {tile_name} to inventory!: {:?}", e); } else { log::info!("Pushed {tile_name} to inventory slot!"); } } log::info!("Setting inventory to initialized!"); *is_initialized = true; } fn show_inventory( q_inventory: Query<&NoonmywaInventory, With>, mut cmd: Commands, q_item_type: Query<&ItemType>, ass: Res, tilesets: Tilesets, ) { let Ok(inventory) = q_inventory.get_single() else { log::warn!("No ControlledByPlayer with inventory found"); return }; let font = ass.load("fonts/FiraSans-Bold.ttf"); let Some(tileset) = tilesets.get_by_name("Noonmywa Tileset") else { log::info!("Tileset doesn't exist yet!"); return }; // Top-level grid (app frame) cmd .spawn((NodeBundle { style: Style { position_type: PositionType::Absolute, display: Display::Grid, left: Val::Percent(25.0), top: Val::Percent(25.0), width: Val::Percent(50.0), height: Val::Percent(50.0), grid_template_columns: vec![GridTrack::min_content(), GridTrack::flex(1.0)], grid_template_rows: vec![ GridTrack::auto(), GridTrack::flex(1.0), GridTrack::px(20.), ], ..default() }, background_color: BackgroundColor(Color::WHITE), ..default() }, //On::>::listener_insert(BackgroundColor::from(colors::BUTTON_HOVERED)) UiInventory, Interaction::None )) .with_children(|builder| { // Header builder .spawn(NodeBundle { focus_policy: bevy::ui::FocusPolicy::Pass, style: Style { display: Display::Grid, /// Make this node span two grid columns so that it takes up the entire top tow grid_column: GridPlacement::span(2), padding: UiRect::all(Val::Px(6.0)), ..default() }, background_color: Color::NONE.into(), ..default() }) .with_children(|builder| { builder.spawn(text_bundle("Inventory", 24.0, Color::WHITE, font.clone())); }); // Main content grid (auto placed in row 2, column 1) builder .spawn(NodeBundle { style: Style { grid_column: GridPlacement::span(2), height: Val::Percent(100.0), //aspect_ratio: Some(1.0), display: Display::Grid, padding: UiRect::all(Val::Px(24.0)), grid_template_columns: RepeatedGridTrack::flex(10, 1.0), grid_template_rows: RepeatedGridTrack::flex(10, 1.0), /// Set a 12px gap/gutter between rows and columns column_gap: Val::Px(12.0), ..default() }, background_color: BackgroundColor(Color::DARK_GRAY), ..default() }) .with_children(|builder| { for (slot, item) in inventory.content() { let Some(item) = item else { log::info!("No item at slot {}", slot); continue; }; let Ok(item_type) = q_item_type.get_component::(item.entity) else { log::info!("Item at slot {} doesn't have an item type!", slot); continue; }; // FIXME instead of going over the tile_images we should go with what is in the // inventory! // In "EDITOR/CREATIVE MODE" this should be all available tiles, but // in the running game, it's... well, what you found so far // FIXME as we cannot get indices of all tiles in the tileset, let's just brute-force a bit match item_type { ItemType::Tile(tile_name) => { if let Some((tile_index, _tile_data)) = tileset.select_tile(tile_name) { tile_rect(builder, &tile_index, tileset.atlas().clone(), slot); } else { log::warn!("No tile found with name: {tile_name}"); } }, ItemType::Item(_item_name) => { // TODO add item_name item_rect(builder, Color::TOMATO); } } } }); // Footer / status bar builder.spawn(NodeBundle { style: Style { // Make this node span two grid column so that it takes up the entire bottom row grid_column: GridPlacement::span(2), ..default() }, background_color: BackgroundColor(Color::WHITE), ..default() }); }); } fn tile_rect( builder: &mut ChildBuilder, tile_index: &TileIndex, texture_atlas: Handle, slot: u8, ) { builder .spawn(( NodeBundle { style: Style { display: Display::Flex, padding: UiRect::all(Val::Px(5.0)), margin: UiRect::all(Val::Px(5.)), width: Val::Px(64.0), height: Val::Px(64.0), ..default() }, ..default() }, UiInventoryItem { slot }, Interaction::None, )) .with_children(|builder| match tile_index { TileIndex::Standard(new_index) => { builder.spawn(AtlasImageBundle { texture_atlas, texture_atlas_image: UiTextureAtlasImage { index: *new_index, ..default() }, ..default() }); } TileIndex::Animated(start, end, speed) => { let animation = SimpleAnimation::from_indices(*start..*end, *speed * 10.0); builder.spawn(( AtlasImageBundle { texture_atlas, texture_atlas_image: UiTextureAtlasImage { index: *start, ..default() }, ..default() }, animation, )); } }); } fn item_rect(builder: &mut ChildBuilder, color: Color) { builder .spawn(NodeBundle { style: Style { display: Display::Flex, padding: UiRect::all(Val::Px(5.0)), ..default() }, background_color: BackgroundColor(Color::BLACK), ..default() }) .with_children(|builder| { builder.spawn(NodeBundle { background_color: BackgroundColor(color), ..default() }); }); }