use crate::{actor, audio, camera, chat, nature, var, world}; use bevy::prelude::*; use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}; use bevy::transform::TransformSystem; use bevy_xpbd_3d::prelude::*; use bevy::math::DVec3; use std::collections::VecDeque; use std::time::SystemTime; pub const HUD_REFRESH_TIME: f32 = 0.1; pub const FONT: &str = "fonts/Yupiter-Regular.ttf"; pub const LOG_MAX_TIME_S: f64 = 30.0; pub const LOG_MAX_ROWS: usize = 30; pub const LOG_MAX: usize = LOG_MAX_ROWS; pub const MAX_CHOICES: usize = 10; pub const AMBIENT_LIGHT: f32 = 0.0; // Space is DARK pub const AMBIENT_LIGHT_AR: f32 = 15.0; //pub const REPLY_NUMBERS: [char; 10] = ['❶', '❷', '❸', '❹', '❺', '❻', '❼', '❽', '❾', '⓿']; //pub const REPLY_NUMBERS: [char; 10] = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩']; pub const REPLY_NUMBERS: [char; 10] = ['➀', '➁', '➂', '➃', '➄', '➅', '➆', '➇', '➈', '➉']; pub struct HudPlugin; impl Plugin for HudPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup); app.add_systems(Update, ( update_hud, handle_input, handle_target_event, )); app.add_systems(PostUpdate, ( update_ar_overlays .after(world::position_to_transform) .in_set(sync::SyncSet::PositionToTransform), update_target_selectagon .after(PhysicsSet::Sync) .after(camera::apply_input_to_player) .before(TransformSystem::TransformPropagate), )); app.insert_resource(AugmentedRealityState { overlays_visible: false, }); app.insert_resource(Log { logs: VecDeque::with_capacity(LOG_MAX), needs_rerendering: true, }); app.insert_resource(FPSUpdateTimer( Timer::from_seconds(HUD_REFRESH_TIME, TimerMode::Repeating))); app.add_event::(); } } #[derive(Event)] pub struct TargetEvent(pub Option); #[derive(Component)] struct NodeHud; #[derive(Component)] struct NodeConsole; #[derive(Component)] struct NodeChoiceText; #[derive(Component)] struct NodeCurrentChatLine; #[derive(Component)] struct Reticule; #[derive(Component)] struct ToggleableHudElement; #[derive(Component)] struct OnlyHideWhenTogglingHud; #[derive(Component)] struct Selectagon; #[derive(Component)] pub struct IsTargeted; #[derive(Resource)] pub struct AugmentedRealityState { pub overlays_visible: bool, } #[derive(Component)] pub struct AugmentedRealityOverlayBroadcaster; #[derive(Component)] pub struct AugmentedRealityOverlay { pub owner: Entity, } #[derive(Resource)] struct FPSUpdateTimer(Timer); pub enum LogLevel { Always, Warning, //Error, Info, //Debug, Chat, //Ping, } struct Message { text: String, sender: String, level: LogLevel, time: f64, } impl Message { pub fn get_freshness(&self) -> f64 { if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { return (1.0 - (epoch.as_secs_f64() - self.time) / LOG_MAX_TIME_S).clamp(0.0, 1.0); } return 1.0; } pub fn format(&self) -> String { if self.sender.is_empty() { return self.text.clone() + "\n"; } else { return format!("{}: {}\n", self.sender, self.text); } } } #[derive(Component)] pub struct IsClickable { pub name: Option, pub distance: Option, } impl Default for IsClickable { fn default() -> Self { Self { name: None, distance: None, }}} #[derive(Resource)] pub struct Log { logs: VecDeque, needs_rerendering: bool, } impl Log { pub fn info(&mut self, message: String) { self.add(message, "System".to_string(), LogLevel::Info); } pub fn chat(&mut self, message: String, sender: String) { self.add(message, sender, LogLevel::Chat); } pub fn warning(&mut self, message: String) { self.add(message, "WARNING".to_string(), LogLevel::Warning); } pub fn add(&mut self, text: String, sender: String, level: LogLevel) { if self.logs.len() == LOG_MAX { self.logs.pop_front(); } if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { self.logs.push_back(Message { text, sender, level, time: epoch.as_secs_f64(), }); self.needs_rerendering = true; } } #[allow(dead_code)] pub fn remove_old(&mut self) { if let Some(message) = self.logs.front() { if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { if epoch.as_secs_f64() - message.time > LOG_MAX_TIME_S { self.logs.pop_front(); } } } } pub fn clear(&mut self) { self.logs.clear(); } } fn setup( mut commands: Commands, settings: Res, asset_server: Res, mut ambient_light: ResMut, ) { let visibility = if settings.hud_active { Visibility::Inherited } else { Visibility::Hidden }; let font_handle = asset_server.load(FONT); let style_conversations = TextStyle { font: font_handle.clone(), font_size: settings.font_size_conversations, color: settings.hud_color_subtitles, ..default() }; let style_console = TextStyle { font: font_handle.clone(), font_size: settings.font_size_console, color: settings.hud_color_console, ..default() }; let style_choices = TextStyle { font: font_handle.clone(), font_size: settings.font_size_choices, color: settings.hud_color_choices, ..default() }; let style = TextStyle { font: font_handle, font_size: settings.font_size_hud, color: settings.hud_color, ..default() }; // Add Statistics HUD let version = &settings.version; let mut bundle_fps = TextBundle::from_sections([ TextSection::new("", style.clone()), TextSection::new(format!(" OutFlyOS v{version} ⚡ "), style.clone()), TextSection::new("", style.clone()), TextSection::new(" ☣ ", style.clone()), TextSection::new("", style.clone()), TextSection::new("", style.clone()), TextSection::new("\n氧 OXYGEN ", style.clone()), TextSection::new("", style.clone()), TextSection::new("\nProximity 警告 ", style.clone()), TextSection::new("", style.clone()), TextSection::new("\nSuit Integrity ", style.clone()), TextSection::new("", style.clone()), TextSection::new("\nVitals ", style.clone()), TextSection::new("", style.clone()), TextSection::new("", style.clone()), // Speed TextSection::new("", style.clone()), // Target ]).with_style(Style { position_type: PositionType::Absolute, top: Val::VMin(2.0), left: Val::VMin(3.0), ..default() }).with_text_justify(JustifyText::Left); bundle_fps.visibility = visibility; commands.spawn(( NodeHud, ToggleableHudElement, bundle_fps, )); // Add Console // This one is intentionally NOT a ToggleableHudElement. Instead, console entries // are filtered based on whether the hud is active or not. LogLevel::Always is // even shown when hud is inactive. let bundle_chatbox = TextBundle::from_sections((0..LOG_MAX_ROWS).map(|_| TextSection::new("", style_console.clone())) ).with_style(Style { position_type: PositionType::Absolute, top: Val::VMin(0.0), right: Val::VMin(0.0), ..default() }).with_text_justify(JustifyText::Right); commands.spawn(( NodeBundle { style: Style { width: Val::Percent(50.0), align_items: AlignItems::Start, position_type: PositionType::Absolute, top: Val::VMin(2.0), right: Val::VMin(3.0), ..default() }, ..default() }, )).with_children(|parent| { parent.spawn(( bundle_chatbox, NodeConsole, )); }); // Add Reticule commands.spawn(( Reticule, ToggleableHudElement, NodeBundle { style: Style { width: Val::Px(2.0), height: Val::Px(2.0), position_type: PositionType::Absolute, top: Val::Vh(50.0), left: Val::Vw(50.0), ..default() }, visibility, background_color: Color::rgb(0.4, 0.4, 0.6).into(), ..default() }, )); // Chat "subtitles" and choices commands.spawn(NodeBundle { style: Style { width: Val::Vw(100.0), align_items: AlignItems::Center, flex_direction: FlexDirection::Column, position_type: PositionType::Absolute, bottom: Val::Vh(2.0), left: Val::Px(0.0), ..default() }, ..default() }).with_children(|builder| { builder.spawn(( TextBundle { text: Text { sections: vec![ TextSection::new("", style_conversations), ], justify: JustifyText::Center, ..default() }, style: Style { max_width: Val::Percent(50.0), margin: UiRect { bottom: Val::Vh(1.0), ..default() }, ..default() }, ..default() }, NodeCurrentChatLine, )); let choice_sections = (0..MAX_CHOICES).map(|_| TextSection::new("", style_choices.clone())); builder.spawn(( TextBundle { text: Text { sections: choice_sections.collect(), ..default() }, ..default() }, NodeChoiceText, )); }); // Selectagon commands.spawn(( Selectagon, ToggleableHudElement, OnlyHideWhenTogglingHud, SceneBundle { scene: asset_server.load(world::asset_name_to_path("selectagon")), visibility: Visibility::Hidden, ..default() }, )); // AR-related things ambient_light.brightness = if settings.hud_active { AMBIENT_LIGHT_AR } else { AMBIENT_LIGHT }; } fn update_hud( diagnostics: Res, time: Res