//! HUD: crosshair, hotbar, debug overlay, controls help. use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}; use bevy::prelude::*; use bevy::window::{PresentMode, PrimaryWindow}; use crate::player::Player; use crate::render::{DayCycle, VoxelMaterials}; use crate::world::block; use crate::world::living::Living; use crate::world::VoxelWorld; pub struct HudPlugin; impl Plugin for HudPlugin { fn build(&self, app: &mut App) { app.add_systems(PostStartup, spawn_hud) .add_systems(Update, (update_hotbar, update_debug, toggle_panels)); } } #[derive(Component)] struct HotbarSlot(usize); #[derive(Component)] struct HotbarIcon(usize); #[derive(Component)] struct SelectedName; #[derive(Component)] struct DebugText; #[derive(Component)] struct HelpText; const HELP: &str = "Click to play | WASD move | Space jump | Shift sprint | F fly (Space/Ctrl up/down)\n\ LMB break | RMB place | MMB pick | 1-9 / wheel slot | Q/E change block\n\ T fast-forward time | +/- view distance | V vsync | G pause living world | C cave culling\n\ F3 debug | F2 screenshot | F11 fullscreen | H hide help | Esc release mouse"; fn text_font(size: f32) -> TextFont { TextFont { font_size: size.into(), ..default() } } fn shadow() -> TextShadow { TextShadow { offset: Vec2::splat(1.5), color: Color::srgba(0.0, 0.0, 0.0, 0.75), } } fn spawn_hud(mut commands: Commands, mats: Res, player: Res) { // Crosshair. commands .spawn(Node { position_type: PositionType::Absolute, width: Val::Percent(100.0), height: Val::Percent(100.0), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }) .with_children(|root| { root.spawn(( Node { width: Val::Px(2.0), height: Val::Px(18.0), position_type: PositionType::Absolute, ..default() }, BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.85)), )); root.spawn(( Node { width: Val::Px(18.0), height: Val::Px(2.0), position_type: PositionType::Absolute, ..default() }, BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.85)), )); }); // Hotbar. commands .spawn(Node { position_type: PositionType::Absolute, width: Val::Percent(100.0), bottom: Val::Px(14.0), flex_direction: FlexDirection::Column, align_items: AlignItems::Center, row_gap: Val::Px(6.0), ..default() }) .with_children(|root| { root.spawn(( SelectedName, Text::new(""), text_font(16.0), TextColor(Color::WHITE), shadow(), )); root.spawn(Node { flex_direction: FlexDirection::Row, column_gap: Val::Px(4.0), padding: UiRect::all(Val::Px(4.0)), ..default() }) .insert(BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.35))) .with_children(|bar| { for i in 0..9 { bar.spawn(( HotbarSlot(i), Node { width: Val::Px(48.0), height: Val::Px(48.0), border: UiRect::all(Val::Px(3.0)), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }, BorderColor::all(Color::srgba(0.0, 0.0, 0.0, 0.5)), BackgroundColor(Color::srgba(0.1, 0.1, 0.12, 0.6)), )) .with_children(|slot| { slot.spawn(( HotbarIcon(i), ImageNode::new(mats.icons[player.hotbar[i] as usize].clone()), Node { width: Val::Px(36.0), height: Val::Px(36.0), ..default() }, )); }); } }); }); commands.spawn(( DebugText, Text::new(""), text_font(15.0), TextColor(Color::WHITE), shadow(), Node { position_type: PositionType::Absolute, left: Val::Px(10.0), top: Val::Px(8.0), ..default() }, )); commands.spawn(( HelpText, Text::new(HELP), text_font(13.0), TextColor(Color::srgba(1.0, 1.0, 1.0, 0.85)), shadow(), Node { position_type: PositionType::Absolute, right: Val::Px(10.0), top: Val::Px(8.0), ..default() }, )); } fn update_hotbar( player: Res, mats: Res, mut slots: Query<(&HotbarSlot, &mut BorderColor)>, mut icons: Query<(&HotbarIcon, &mut ImageNode)>, mut name: Query<&mut Text, With>, ) { if !player.is_changed() { return; } for (slot, mut border) in &mut slots { *border = if slot.0 == player.slot { BorderColor::all(Color::srgb(1.0, 0.92, 0.55)) } else { BorderColor::all(Color::srgba(0.0, 0.0, 0.0, 0.5)) }; } for (icon, mut image) in &mut icons { let handle = &mats.icons[player.hotbar[icon.0] as usize]; if image.image != *handle { image.image = handle.clone(); } } if let Ok(mut text) = name.single_mut() { text.0 = block::info(player.selected()).name.to_string(); } } #[allow(clippy::too_many_arguments)] fn update_debug( diagnostics: Res, world: Res, player: Res, cycle: Res, living: Res, windows: Query<&Window, With>, mut text: Query<&mut Text, With>, time: Res