use crate::{actor, audio, camera, chat, nature, settings, 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: usize = 20; pub const LOG_MAX_TIME_S: u64 = 20; 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, update_ar_overlays, handle_input, handle_target_event, )); app.add_systems(PostUpdate, ( 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 GaugesText; #[derive(Component)] struct ChatText; #[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 { Warning, //Error, Info, //Debug, Chat, //Ping, Notice, } struct Message { text: String, sender: String, level: LogLevel, time: u64, } #[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 notice(&mut self, message: String) { self.add(message, "".to_string(), LogLevel::Notice); } pub fn add(&mut self, message: 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: message, sender: sender, level: level, time: epoch.as_secs(), }); self.needs_rerendering = true; } } 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() - 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 log: ResMut, mut ambient_light: ResMut, ) { log.notice("Resuming from suspend".to_string()); log.notice("WARNING: Oxygen Low".to_string()); let visibility = if settings.hud_active { Visibility::Inherited } else { Visibility::Hidden }; let mut bundle_fps = TextBundle::from_sections([ TextSection::new( "", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() } ), TextSection::new( " ⚡ ", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() }, ), TextSection::new( "", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() } ), TextSection::new( " ☣ ", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() }, ), TextSection::new( "", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() } ), TextSection::new( "", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() } ), TextSection::new( "\n氧 OXYGEN ", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() }, ), TextSection::new( "", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::MAROON, ..default() } ), TextSection::new( "\nProximity 警告 ", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() }, ), TextSection::new( "", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() } ), TextSection::new( "\nSuit Integrity ", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() }, ), TextSection::new( "", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() } ), TextSection::new( "\nVitals ", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() }, ), TextSection::new( "", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() } ), TextSection::new( // Target "", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::GRAY, ..default() } ), ]).with_style(Style { top: Val::VMin(2.0), left: Val::VMin(3.0), ..default() }); bundle_fps.visibility = visibility; commands.spawn(( GaugesText, ToggleableHudElement, bundle_fps, )); // Add Chat Box let bundle_chatbox = TextBundle::from_sections([ TextSection::new( "Warning: System Log Uninitialized", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::rgb(0.7, 0.7, 0.7), ..default() } ), TextSection::new( "\n", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::WHITE, ..default() } ), TextSection::new( "\n\n\n", TextStyle { font: asset_server.load(FONT), font_size: settings.font_size_hud, color: Color::WHITE, ..default() } ), ]).with_style(Style { position_type: PositionType::Absolute, bottom: Val::VMin(0.0), left: Val::VMin(0.0), ..default() }).with_text_justify(JustifyText::Left); commands.spawn(( NodeBundle { style: Style { width: Val::Percent(50.), align_items: AlignItems::Start, position_type: PositionType::Absolute, bottom: Val::Vh(10.0), left: Val::Vw(25.0), ..default() }, ..default() }, )).with_children(|parent| { parent.spawn(( bundle_chatbox, ChatText, )); }); 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: visibility, background_color: Color::rgb(0.4, 0.4, 0.6).into(), ..default() }, )); // 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