outfly/src/hud.rs

1126 lines
39 KiB
Rust
Raw Normal View History

2024-04-21 16:23:40 +00:00
// ▄████████▄ + ███ + ▄█████████ ███ +
// ███▀ ▀███ + + ███ ███▀ + ███ + +
// ███ + ███ ███ ███ █████████ ███ ███ ███ ███
2024-04-21 17:34:00 +00:00
// ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███
2024-04-21 16:23:40 +00:00
// ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███
// ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███
// ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████
// + + + ███
// + ▀████████████████████████████████████████████████████▀
2024-04-23 15:33:36 +00:00
//
// This module manages the heads-up display and augmented reality overlays.
2024-04-21 16:23:40 +00:00
use crate::{actor, audio, camera, chat, nature, skeleton, var};
2024-05-07 19:05:51 +00:00
use bevy::pbr::{NotShadowCaster, NotShadowReceiver};
2024-03-17 14:23:22 +00:00
use bevy::prelude::*;
use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
2024-04-05 20:16:01 +00:00
use bevy::transform::TransformSystem;
2024-03-30 16:19:11 +00:00
use bevy_xpbd_3d::prelude::*;
use bevy::math::DVec3;
2024-03-17 23:36:56 +00:00
use std::collections::VecDeque;
2024-03-18 00:02:17 +00:00
use std::time::SystemTime;
2024-03-17 14:23:22 +00:00
2024-04-08 00:26:23 +00:00
pub const HUD_REFRESH_TIME: f32 = 0.1;
2024-03-22 13:35:07 +00:00
pub const FONT: &str = "fonts/Yupiter-Regular.ttf";
2024-04-15 18:58:19 +00:00
pub const LOG_MAX_TIME_S: f64 = 30.0;
pub const LOG_MAX_ROWS: usize = 30;
2024-04-15 21:17:44 +00:00
pub const LOG_MAX: usize = LOG_MAX_ROWS;
2024-04-15 18:58:19 +00:00
pub const MAX_CHOICES: usize = 10;
2024-05-07 22:15:34 +00:00
pub const SPEEDOMETER_WIDTH: f32 = 20.0;
pub const SPEEDOMETER_HEIGHT: f32 = 10.0;
pub const AMBIENT_LIGHT: f32 = 0.0; // Space is DARK
2024-05-01 20:26:02 +00:00
pub const AMBIENT_LIGHT_AR: f32 = 20.0;
2024-03-20 17:54:22 +00:00
//pub const REPLY_NUMBERS: [char; 10] = ['❶', '❷', '❸', '❹', '❺', '❻', '❼', '❽', '❾', '⓿'];
2024-03-22 13:38:28 +00:00
//pub const REPLY_NUMBERS: [char; 10] = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'];
pub const REPLY_NUMBERS: [char; 10] = ['➀', '➁', '➂', '➃', '➄', '➅', '➆', '➇', '➈', '➉'];
2024-03-17 20:57:30 +00:00
2024-03-17 22:49:50 +00:00
pub struct HudPlugin;
impl Plugin for HudPlugin {
2024-03-17 14:23:22 +00:00
fn build(&self, app: &mut App) {
app.add_systems(Startup, setup);
app.add_systems(Update, (
2024-04-07 18:02:31 +00:00
update_hud,
update_dashboard,
update_speedometer,
update_gauges,
handle_input,
handle_target_event,
2024-04-05 20:16:01 +00:00
));
app.add_systems(PostUpdate, (
update_overlay_visibility,
2024-04-18 20:48:26 +00:00
update_ar_overlays
.after(camera::position_to_transform)
2024-04-18 20:48:26 +00:00
.in_set(sync::SyncSet::PositionToTransform),
update_poi_overlays
.after(camera::position_to_transform)
.in_set(sync::SyncSet::PositionToTransform),
2024-04-05 20:16:01 +00:00
update_target_selectagon
.after(PhysicsSet::Sync)
.after(camera::apply_input_to_player)
.before(TransformSystem::TransformPropagate),
));
2024-04-10 19:03:30 +00:00
app.insert_resource(AugmentedRealityState {
overlays_visible: false,
});
app.insert_resource(Log {
logs: VecDeque::with_capacity(LOG_MAX),
needs_rerendering: true,
});
2024-03-17 20:57:30 +00:00
app.insert_resource(FPSUpdateTimer(
Timer::from_seconds(HUD_REFRESH_TIME, TimerMode::Repeating)));
app.add_event::<TargetEvent>();
app.add_event::<UpdateOverlayVisibility>();
2024-03-17 14:23:22 +00:00
}
}
#[derive(Event)] pub struct TargetEvent(pub Option<Entity>);
#[derive(Event)] pub struct UpdateOverlayVisibility;
2024-04-15 18:58:19 +00:00
#[derive(Component)] struct NodeHud;
#[derive(Component)] struct NodeConsole;
#[derive(Component)] struct NodeChoiceText;
2024-04-28 04:29:01 +00:00
#[derive(Component)] struct NodeSpeedometerText;
2024-04-15 18:58:19 +00:00
#[derive(Component)] struct NodeCurrentChatLine;
2024-03-28 19:47:18 +00:00
#[derive(Component)] struct Reticule;
2024-04-28 01:15:45 +00:00
#[derive(Component)] struct Speedometer;
#[derive(Component)] struct Speedometer2;
#[derive(Component)] struct Dashboard;
#[derive(Component)] struct DashboardFlashlight;
#[derive(Component)] struct GaugeLength(f32);
#[derive(Component)] pub struct ToggleableHudElement;
#[derive(Component)] pub struct ToggleableHudMapElement;
2024-04-05 20:16:01 +00:00
#[derive(Component)] struct Selectagon;
#[derive(Component)] pub struct IsTargeted;
#[derive(Component)] pub struct PointOfInterestMarker(pub Entity);
2024-03-17 14:23:22 +00:00
#[derive(Component, Debug, Copy, Clone)]
enum Gauge {
Health,
Power,
Oxygen,
2024-05-07 22:33:49 +00:00
Integrity,
}
2024-04-10 19:03:30 +00:00
#[derive(Resource)]
pub struct AugmentedRealityState {
pub overlays_visible: bool,
}
#[derive(Component)] pub struct AugmentedRealityOverlayBroadcaster;
#[derive(Component)]
pub struct AugmentedRealityOverlay {
pub owner: Entity,
}
2024-03-17 14:23:22 +00:00
#[derive(Resource)]
struct FPSUpdateTimer(Timer);
pub enum LogLevel {
2024-04-15 21:17:44 +00:00
Always,
Warning,
//Error,
Info,
//Debug,
Chat,
//Ping,
}
2024-03-18 00:02:17 +00:00
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;
}
2024-04-15 18:58:19 +00:00
pub fn format(&self) -> String {
if self.sender.is_empty() {
return self.text.clone() + "\n";
}
else {
return format!("{}: {}\n", self.sender, self.text);
}
}
2024-03-18 00:02:17 +00:00
}
2024-04-07 22:39:57 +00:00
#[derive(Component)]
pub struct IsClickable {
pub name: Option<String>,
2024-04-20 00:48:55 +00:00
pub pronoun: Option<String>,
2024-04-07 22:39:57 +00:00
pub distance: Option<f64>,
}
impl Default for IsClickable { fn default() -> Self { Self {
name: None,
2024-04-20 00:48:55 +00:00
pronoun: None,
2024-04-07 22:39:57 +00:00
distance: None,
}}}
2024-03-17 23:36:56 +00:00
#[derive(Resource)]
pub struct Log {
2024-03-18 00:02:17 +00:00
logs: VecDeque<Message>,
needs_rerendering: bool,
2024-03-17 23:36:56 +00:00
}
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);
}
2024-04-14 19:52:18 +00:00
pub fn add(&mut self, text: String, sender: String, level: LogLevel) {
2024-03-17 23:36:56 +00:00
if self.logs.len() == LOG_MAX {
self.logs.pop_front();
}
2024-03-18 00:02:17 +00:00
if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
self.logs.push_back(Message {
2024-04-14 19:52:18 +00:00
text,
sender,
level,
time: epoch.as_secs_f64(),
2024-03-18 00:02:17 +00:00
});
self.needs_rerendering = true;
2024-03-18 00:02:17 +00:00
}
}
#[allow(dead_code)]
2024-03-18 00:02:17 +00:00
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 {
2024-03-18 00:02:17 +00:00
self.logs.pop_front();
}
}
}
2024-03-17 23:36:56 +00:00
}
2024-04-05 03:13:09 +00:00
pub fn clear(&mut self) {
self.logs.clear();
}
2024-03-17 23:36:56 +00:00
}
2024-03-17 14:23:22 +00:00
fn setup(
mut commands: Commands,
settings: Res<var::Settings>,
asset_server: Res<AssetServer>,
mut ew_updateoverlays: EventWriter<UpdateOverlayVisibility>,
2024-03-17 14:23:22 +00:00
) {
let visibility = if settings.hud_active {
2024-03-17 18:03:02 +00:00
Visibility::Inherited
} else {
Visibility::Hidden
};
2024-04-15 18:58:19 +00:00
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()
};
2024-04-28 04:29:01 +00:00
let style_speedometer = TextStyle {
font: font_handle.clone(),
font_size: settings.font_size_speedometer,
color: settings.hud_color_speedometer,
..default()
};
2024-04-15 00:27:11 +00:00
let style = TextStyle {
2024-04-15 18:58:19 +00:00
font: font_handle,
2024-04-15 00:27:11 +00:00
font_size: settings.font_size_hud,
2024-04-15 18:58:19 +00:00
color: settings.hud_color,
2024-04-15 00:27:11 +00:00
..default()
};
2024-04-15 18:58:19 +00:00
// Add Statistics HUD
2024-04-18 01:56:32 +00:00
let version = &settings.version;
2024-03-17 18:03:02 +00:00
let mut bundle_fps = TextBundle::from_sections([
2024-04-15 00:27:11 +00:00
TextSection::new("", style.clone()),
TextSection::new(format!(" OutFlyOS v{version}"), style.clone()),
2024-04-15 00:27:11 +00:00
TextSection::new("", style.clone()),
TextSection::new("", style.clone()), // Speed
2024-04-15 00:27:11 +00:00
TextSection::new("", style.clone()), // Target
2024-03-17 23:42:10 +00:00
]).with_style(Style {
2024-04-15 00:42:23 +00:00
position_type: PositionType::Absolute,
2024-03-18 00:23:35 +00:00
top: Val::VMin(2.0),
2024-04-15 18:58:19 +00:00
left: Val::VMin(3.0),
2024-03-17 23:42:10 +00:00
..default()
2024-04-15 18:58:19 +00:00
}).with_text_justify(JustifyText::Left);
2024-03-17 18:03:02 +00:00
bundle_fps.visibility = visibility;
2024-03-17 14:23:22 +00:00
commands.spawn((
2024-04-15 18:58:19 +00:00
NodeHud,
2024-03-28 19:47:18 +00:00
ToggleableHudElement,
bundle_fps,
2024-03-17 14:23:22 +00:00
));
2024-04-15 18:58:19 +00:00
// Add Console
2024-04-15 21:17:44 +00:00
// 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.
2024-04-15 18:58:19 +00:00
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),
2024-04-15 18:58:19 +00:00
right: Val::VMin(0.0),
..default()
2024-04-15 18:58:19 +00:00
}).with_text_justify(JustifyText::Right);
commands.spawn((
2024-03-19 14:51:08 +00:00
NodeBundle {
style: Style {
2024-04-15 18:58:19 +00:00
width: Val::Percent(50.0),
2024-03-19 14:51:08 +00:00
align_items: AlignItems::Start,
position_type: PositionType::Absolute,
top: Val::VMin(2.0),
2024-04-15 18:58:19 +00:00
right: Val::VMin(3.0),
2024-03-19 14:51:08 +00:00
..default()
},
..default()
},
)).with_children(|parent| {
parent.spawn((
bundle_chatbox,
2024-04-15 18:58:19 +00:00
NodeConsole,
2024-03-19 14:51:08 +00:00
));
});
2024-04-15 18:58:19 +00:00
// Add Reticule
let reticule_handle: Handle<Image> = asset_server.load("sprites/reticule4.png");
commands.spawn((
NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceAround,
..default()
},
visibility,
2024-03-28 19:47:18 +00:00
..default()
},
ToggleableHudElement,
)).with_children(|builder| {
2024-04-27 23:33:27 +00:00
builder.spawn((
ImageBundle {
image: UiImage::new(reticule_handle),
style: Style {
width: Val::VMin(5.0),
height: Val::VMin(5.0),
2024-04-27 23:33:27 +00:00
..Default::default()
},
..Default::default()
},
Reticule,
));
});
2024-03-28 19:47:18 +00:00
2024-05-07 22:33:49 +00:00
// HP/O2/Suit Integrity/Power Gauges
let gauges_handle: Handle<Image> = asset_server.load("sprites/gauge_horizontal.png");
let gauges = [
2024-05-07 23:00:06 +00:00
("sprites/gauge_heart.png", Gauge::Health),
("sprites/gauge_battery.png", Gauge::Power),
("sprites/gauge_o2.png", Gauge::Oxygen),
("sprites/gauge_suit.png", Gauge::Integrity),
];
2024-05-07 23:00:06 +00:00
let icon_size = 24.0;
let gauge_bar_padding_left = 4.0;
for (i, (sprite, gauge)) in gauges.iter().enumerate() {
let bar_length = if i == 0 { 196.0 } else { 128.0 };
// The bar with variable width
commands.spawn((
NodeBundle {
style: Style {
width: Val::Percent(30.0),
height: Val::Percent(100.0),
bottom: Val::Px(20.0 + 24.0 * i as f32),
left: Val::VMin(2.0),
align_items: AlignItems::End,
overflow: Overflow::clip(),
..default()
},
visibility,
..default()
},
Dashboard,
ToggleableHudElement,
)).with_children(|builder| {
builder.spawn((
NodeBundle {
style: Style {
width: Val::Px(118.0),
height: Val::Px(10.0),
bottom: Val::Px(8.0),
2024-05-07 23:00:06 +00:00
left: Val::Px(gauge_bar_padding_left + icon_size),
..Default::default()
},
visibility,
background_color: settings.hud_color.into(),
..Default::default()
},
gauge.clone(),
GaugeLength(bar_length),
));
});
// The decorator sprites surrounding the bar
commands.spawn((
NodeBundle {
style: Style {
width: Val::Percent(30.0),
height: Val::Percent(100.0),
bottom: Val::Px(20.0 + 24.0 * i as f32),
left: Val::VMin(2.0),
align_items: AlignItems::End,
overflow: Overflow::clip(),
..default()
},
visibility,
..default()
},
Dashboard,
ToggleableHudElement,
)).with_children(|builder| {
2024-05-07 23:00:06 +00:00
// The gauge symbol
builder.spawn((
ImageBundle {
image: UiImage::new(asset_server.load(sprite.to_string())),
style: Style {
2024-05-07 23:00:06 +00:00
width: Val::Px(icon_size),
height: Val::Px(icon_size),
..Default::default()
},
visibility,
..Default::default()
},
));
2024-05-07 23:00:06 +00:00
// The gauge bar border
builder.spawn((
ImageBundle {
image: UiImage::new(gauges_handle.clone()),
style: Style {
width: Val::Px(bar_length),
height: Val::Px(10.0),
bottom: Val::Px(8.0),
2024-05-07 23:00:06 +00:00
left: Val::Px(gauge_bar_padding_left),
..Default::default()
},
visibility,
..Default::default()
},
));
});
}
// Car-Dashboard-Style icons
let flashlight_visibility = bool2vis(visibility == Visibility::Visible && settings.flashlight_active);
let dashboard_flashlight_handle: Handle<Image> = asset_server.load("sprites/dashboard_highbeams.png");
commands.spawn((
NodeBundle {
style: Style {
width: Val::Percent(30.0),
height: Val::Percent(100.0),
bottom: Val::VMin(SPEEDOMETER_HEIGHT + 3.0),
left: Val::VMin(4.0),
align_items: AlignItems::End,
overflow: Overflow::clip(),
..default()
},
visibility,
..default()
},
Dashboard,
ToggleableHudElement,
)).with_children(|builder| {
builder.spawn((
ImageBundle {
image: UiImage::new(dashboard_flashlight_handle),
style: Style {
width: Val::VMin(6.0),
height: Val::VMin(6.0),
..Default::default()
},
visibility: flashlight_visibility,
..Default::default()
},
DashboardFlashlight,
));
});
2024-04-28 01:15:45 +00:00
// Add Speedometer
let speedometer_handle: Handle<Image> = asset_server.load("sprites/speedometer.png");
2024-04-28 01:15:45 +00:00
commands.spawn((
NodeBundle {
style: Style {
width: Val::VMin(0.0),
2024-04-28 01:15:45 +00:00
height: Val::Percent(100.0),
2024-05-07 22:15:34 +00:00
left: Val::Vw(100.0 - SPEEDOMETER_WIDTH),
2024-04-28 01:15:45 +00:00
align_items: AlignItems::End,
overflow: Overflow::clip(),
..default()
},
visibility,
..default()
},
Speedometer,
ToggleableHudElement,
)).with_children(|builder| {
builder.spawn((
ImageBundle {
image: UiImage::new(speedometer_handle),
style: Style {
2024-05-07 22:15:34 +00:00
width: Val::Vw(SPEEDOMETER_WIDTH),
height: Val::VMin(SPEEDOMETER_HEIGHT),
..Default::default()
},
..Default::default()
},
));
});
let speedometer_handle: Handle<Image> = asset_server.load("sprites/speedometer_white.png");
commands.spawn((
NodeBundle {
style: Style {
width: Val::VMin(0.0),
height: Val::Percent(100.0),
2024-05-07 22:15:34 +00:00
left: Val::Vw(100.0 - SPEEDOMETER_WIDTH),
align_items: AlignItems::End,
overflow: Overflow::clip(),
..default()
},
visibility,
..default()
},
Speedometer2,
ToggleableHudElement,
)).with_children(|builder| {
builder.spawn((
ImageBundle {
image: UiImage::new(speedometer_handle),
2024-04-28 01:15:45 +00:00
style: Style {
2024-05-07 22:15:34 +00:00
width: Val::Vw(SPEEDOMETER_WIDTH),
height: Val::VMin(SPEEDOMETER_HEIGHT),
2024-04-28 01:15:45 +00:00
..Default::default()
},
..Default::default()
},
));
});
2024-04-28 04:29:01 +00:00
let mut bundle_speedometer_text = TextBundle::from_sections([
2024-05-07 23:35:28 +00:00
TextSection::new("", style_speedometer.clone()), // speed relative to target
2024-04-28 04:29:01 +00:00
TextSection::new("", style_speedometer.clone()), // speed relative to target
TextSection::new("", style_speedometer.clone()), // speed relative to orbit
]).with_style(Style {
position_type: PositionType::Absolute,
2024-05-07 22:15:34 +00:00
left: Val::Vw(100.0 - SPEEDOMETER_WIDTH + 2.0),
2024-04-28 04:29:01 +00:00
bottom: Val::VMin(4.0),
..default()
}).with_text_justify(JustifyText::Left);
bundle_speedometer_text.visibility = visibility;
commands.spawn((
NodeSpeedometerText,
ToggleableHudElement,
bundle_speedometer_text,
));
2024-04-28 01:15:45 +00:00
2024-04-15 18:58:19 +00:00
// 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,
));
});
2024-04-05 20:16:01 +00:00
// Selectagon
2024-04-22 19:01:27 +00:00
let mut entitycmd = commands.spawn((
2024-04-05 20:16:01 +00:00
Selectagon,
2024-05-07 19:05:51 +00:00
NotShadowCaster,
NotShadowReceiver,
2024-04-22 19:01:27 +00:00
SpatialBundle {
2024-04-05 20:43:14 +00:00
visibility: Visibility::Hidden,
2024-04-05 20:16:01 +00:00
..default()
},
));
2024-04-22 19:01:27 +00:00
skeleton::load("selectagon", &mut entitycmd, &*asset_server);
2024-04-05 20:16:01 +00:00
ew_updateoverlays.send(UpdateOverlayVisibility);
2024-03-17 14:23:22 +00:00
}
fn update_dashboard(
timer: ResMut<FPSUpdateTimer>,
mut q_flashlight: Query<&mut Visibility, With<DashboardFlashlight>>,
settings: Res<var::Settings>,
) {
if !timer.0.just_finished() {
return;
}
for mut vis in &mut q_flashlight {
*vis = bool2vis(settings.flashlight_active);
}
}
fn update_speedometer(
timer: ResMut<FPSUpdateTimer>,
q_camera: Query<&LinearVelocity, With<actor::PlayerCamera>>,
2024-05-07 23:35:28 +00:00
q_player: Query<&actor::ExperiencesGForce, With<actor::Player>>,
2024-04-28 04:29:01 +00:00
q_target: Query<&LinearVelocity, With<IsTargeted>>,
mut q_speedometer: Query<&mut Style, (With<Speedometer>, Without<Speedometer2>)>,
mut q_speedometer2: Query<&mut Style, (With<Speedometer2>, Without<Speedometer>)>,
2024-04-28 04:29:01 +00:00
mut q_node_speed: Query<&mut Text, With<NodeSpeedometerText>>,
) {
if !timer.0.just_finished() {
return;
}
if let Ok(cam_v) = q_camera.get_single() {
let speed = cam_v.length();
let speedometer_split = 5_000.0;
if let Ok(mut speedometer) = q_speedometer.get_single_mut() {
let custom_c = speedometer_split;
let fraction = nature::lorenz_factor_custom_c((custom_c - speed).clamp(0.0, custom_c), custom_c).clamp(0.0, 1.0) as f32;
let wid = (fraction * SPEEDOMETER_WIDTH).clamp(0.0, 100.0);
2024-05-07 22:15:34 +00:00
speedometer.width = Val::Vw(wid);
}
if let Ok(mut speedometer2) = q_speedometer2.get_single_mut() {
let custom_c = nature::C - speedometer_split;
let fraction = nature::lorenz_factor_custom_c((custom_c - speed + speedometer_split).clamp(0.0, custom_c), custom_c).clamp(0.0, 1.0) as f32;
let wid = (fraction * SPEEDOMETER_WIDTH).clamp(0.0, 100.0);
2024-05-07 22:15:34 +00:00
speedometer2.width = Val::Vw(wid);
}
2024-04-28 04:29:01 +00:00
if let Ok(mut speed_text) = q_node_speed.get_single_mut() {
2024-05-07 23:35:28 +00:00
// G forces
speed_text.sections[0].value = if let Ok(gforce) = q_player.get_single() {
if gforce.gforce > 0.0001 {
format!("{:.1}g\n", gforce.gforce)
} else {
"".to_string()
}
} else {
"".to_string()
};
// Velocity relative to target
speed_text.sections[1].value = if let Ok(target_v) = q_target.get_single() {
2024-04-28 04:29:01 +00:00
let delta_v = (target_v.0 - cam_v.0).length();
2024-04-28 04:40:11 +00:00
if delta_v > 0.0001 {
format!("Δv {}\n", nature::readable_speed(delta_v))
} else {
"".to_string()
}
} else {
"".to_string()
};
2024-05-07 23:35:28 +00:00
// "Absolute velocity", or velocity relative to orbit
speed_text.sections[2].value = if speed > 0.0001 {
2024-04-28 04:40:11 +00:00
nature::readable_speed(speed)
2024-04-28 04:29:01 +00:00
} else {
"".to_string()
};
}
}
}
fn update_gauges(
timer: ResMut<FPSUpdateTimer>,
2024-05-07 22:33:49 +00:00
q_player: Query<(&actor::HitPoints, &actor::Suit), With<actor::Player>>,
mut q_gauges: Query<(&mut Style, &mut BackgroundColor, &Gauge, &GaugeLength)>,
settings: Res<var::Settings>,
) {
if !timer.0.just_finished() {
return;
}
2024-05-07 22:33:49 +00:00
let player = q_player.get_single();
if player.is_err() { return; }
let (hp, suit) = player.unwrap();
for (mut style, mut bg, gauge, len) in &mut q_gauges {
2024-05-07 22:33:49 +00:00
let value: f32 = match gauge {
Gauge::Health => hp.current / hp.max,
Gauge::Oxygen => (suit.oxygen / suit.oxygen_max).powf(0.5),
2024-05-07 22:33:49 +00:00
Gauge::Integrity => suit.integrity,
Gauge::Power => suit.power / suit.power_max,
};
if value < 0.5 {
*bg = settings.hud_color_alert.into();
}
else {
*bg = settings.hud_color.into();
}
style.width = Val::Px(len.0 * value);
}
}
2024-04-07 18:02:31 +00:00
fn update_hud(
2024-03-17 14:23:22 +00:00
diagnostics: Res<DiagnosticsStore>,
2024-03-17 22:49:50 +00:00
time: Res<Time>,
2024-03-18 00:02:17 +00:00
mut log: ResMut<Log>,
q_camera: Query<(&Position, &LinearVelocity), With<actor::PlayerCamera>>,
2024-03-17 14:23:22 +00:00
mut timer: ResMut<FPSUpdateTimer>,
2024-04-13 13:26:45 +00:00
q_choices: Query<&chat::Choice>,
2024-04-15 18:58:19 +00:00
q_chat: Query<&chat::Chat>,
mut q_node_hud: Query<&mut Text, With<NodeHud>>,
mut q_node_console: Query<&mut Text, (With<NodeConsole>, Without<NodeHud>, Without<NodeChoiceText>)>,
mut q_node_choice: Query<&mut Text, (With<NodeChoiceText>, Without<NodeHud>, Without<NodeConsole>)>,
mut q_node_currentline: Query<&mut Text, (With<NodeCurrentChatLine>, Without<NodeHud>, Without<NodeConsole>, Without<NodeChoiceText>)>,
settings: Res<var::Settings>,
q_target: Query<(&IsClickable, Option<&Position>, Option<&LinearVelocity>), With<IsTargeted>>,
2024-03-17 14:23:22 +00:00
) {
2024-03-18 03:57:17 +00:00
// TODO only when hud is actually on
if timer.0.tick(time.delta()).just_finished() || log.needs_rerendering {
2024-03-30 16:19:11 +00:00
let q_camera_result = q_camera.get_single();
let mut freshest_line: f64 = 0.0;
2024-05-07 23:35:28 +00:00
if q_camera_result.is_ok() {
2024-04-28 04:29:01 +00:00
let (pos, _) = q_camera_result.unwrap();
2024-04-15 18:58:19 +00:00
for mut text in &mut q_node_hud {
2024-04-08 02:16:01 +00:00
text.sections[0].value = format!("2524-03-12 03:02");
2024-03-17 22:49:50 +00:00
if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
if let Some(value) = fps.smoothed() {
// Update the value of the second section
text.sections[2].value = format!("{value:.0}");
2024-03-17 22:49:50 +00:00
}
2024-03-17 14:23:22 +00:00
}
// Target display
2024-04-08 02:16:01 +00:00
let dist_scalar: f64;
let mut target_multiple = false;
let mut target_error = false;
if let Ok((IsClickable { distance: Some(dist), .. }, _, _)) = q_target.get_single() {
dist_scalar = *dist;
}
else {
2024-04-07 22:39:57 +00:00
let target: Option<DVec3>;
if let Ok((_, Some(targetpos), _)) = q_target.get_single() {
2024-04-07 22:39:57 +00:00
target = Some(targetpos.0);
}
else if q_target.is_empty() {
target = Some(DVec3::new(0.0, 0.0, 0.0));
}
else if q_target.iter().len() > 1 {
target_multiple = true;
target = None;
}
2024-04-07 22:39:57 +00:00
else {
target_error = true;
2024-04-07 22:39:57 +00:00
target = None;
}
if let Some(target_pos) = target {
let dist = pos.0 - target_pos;
dist_scalar = dist.length();
}
else {
dist_scalar = 0.0;
}
}
2024-04-07 22:39:57 +00:00
if target_multiple {
text.sections[4].value = "\n\nERROR: MULTIPLE TARGETS".to_string();
}
else if target_error {
text.sections[4].value = "\n\nERROR: FAILED TO AQUIRE TARGET".to_string();
}
2024-04-28 04:29:01 +00:00
else if let Ok((clickable, _, _)) = q_target.get_single() {
2024-04-08 02:27:02 +00:00
let distance = if dist_scalar.is_nan() {
"UNKNOWN".to_string()
} else if dist_scalar != 0.0 {
nature::readable_distance(dist_scalar)
} else {
"ERROR".to_string()
};
let target_name = clickable.name.clone().unwrap_or("Unnamed".to_string());
2024-04-20 01:07:28 +00:00
let pronoun = if let Some(pronoun) = &clickable.pronoun {
format!("Pronoun: {pronoun}\n")
} else {
"".to_string()
};
text.sections[4].value = format!("\n\nTarget: {target_name}\n{pronoun}Distance: {distance}");
}
else {
text.sections[4].value = "".to_string();
}
2024-03-17 14:23:22 +00:00
}
}
2024-04-15 18:58:19 +00:00
let chat = q_node_console.get_single_mut();
if chat.is_err() {
error!("Couldn't find HUD UI text section");
return;
}
let mut chat = chat.unwrap();
2024-04-15 18:58:19 +00:00
let choicebox = q_node_choice.get_single_mut();
if choicebox.is_err() {
error!("Couldn't find HUD UI text section");
return;
}
let mut choicebox = choicebox.unwrap();
let node_currentline = q_node_currentline.get_single_mut();
if node_currentline.is_err() {
error!("Couldn't find HUD UI text section");
return;
}
let mut node_currentline = node_currentline.unwrap();
let mut row = 0;
// Chat Log and System Log
2024-04-15 21:17:44 +00:00
let logfilter = if settings.hud_active {
|_msg: &&Message| { true }
} else {
|msg: &&Message| { match msg.level {
LogLevel::Always => true,
_ => false
}}
};
2024-04-15 18:58:19 +00:00
let messages: Vec<&Message> = log.logs.iter()
2024-04-15 21:17:44 +00:00
.filter(logfilter)
2024-04-15 18:58:19 +00:00
.rev()
.take(LOG_MAX_ROWS)
2024-04-15 18:58:19 +00:00
.collect();
//messages.reverse();
for msg in &messages {
chat.sections[row].value = msg.format();
let freshness = msg.get_freshness();
let opacity: f32 = (freshness.powf(1.5) as f32).clamp(0.0, 1.0);
2024-04-15 18:58:19 +00:00
freshest_line = freshest_line.max(freshness);
chat.sections[row].style.color = match msg.level {
LogLevel::Warning => settings.hud_color_console_warn,
LogLevel::Info => settings.hud_color_console_system,
_ => settings.hud_color_console,
};
2024-04-15 21:23:46 +00:00
chat.sections[row].style.color.set_a(opacity);
2024-04-15 18:58:19 +00:00
row += 1;
}
// Display the last chat line as "subtitles"
if !q_chat.is_empty() {
let messages: Vec<&Message> = log.logs.iter()
.filter(|msg: &&Message| { match msg.level {
LogLevel::Chat => true,
_ => false
2024-04-15 18:58:19 +00:00
}})
.rev()
2024-04-15 18:58:19 +00:00
.take(1)
.collect();
2024-04-15 18:58:19 +00:00
if messages.len() > 0 {
node_currentline.sections[0].value = messages[0].format();
} else {
node_currentline.sections[0].value = "".to_string();
}
2024-04-15 18:58:19 +00:00
} else {
node_currentline.sections[0].value = "".to_string();
}
2024-04-15 18:58:19 +00:00
// Blank the remaining rows
while row < LOG_MAX_ROWS {
chat.sections[row].value = "".to_string();
row += 1;
2024-04-15 18:58:19 +00:00
}
2024-04-15 18:58:19 +00:00
// Choices
row = 0;
let mut choices: Vec<String> = Vec::new();
let mut count = 0;
for choice in &q_choices {
if count > 9 {
break;
2024-04-13 13:26:45 +00:00
}
2024-04-15 18:58:19 +00:00
let press_this = REPLY_NUMBERS[choice.key];
let reply = &choice.text;
//let recipient = &choice.recipient;
// TODO: indicate recipients if there's more than one
choices.push(format!("{press_this} {reply}"));
count += 1;
}
for choice in choices {
if row >= MAX_CHOICES {
break;
}
2024-04-15 18:58:19 +00:00
choicebox.sections[row].value = choice + "\n";
row += 1;
}
2024-04-15 18:58:19 +00:00
while row < MAX_CHOICES {
choicebox.sections[row].value = if row < 4 {
" \n".to_string()
} else {
"".to_string()
};
row += 1;
}
2024-04-15 18:58:19 +00:00
log.needs_rerendering = false;
2024-04-15 18:58:19 +00:00
//if q_choices.is_empty() && freshest_line < 0.2 {
log.remove_old();
//}
2024-03-17 14:23:22 +00:00
}
}
2024-03-17 18:03:02 +00:00
fn handle_input(
keyboard_input: Res<ButtonInput<KeyCode>>,
mouse_input: Res<ButtonInput<MouseButton>>,
mut settings: ResMut<var::Settings>,
2024-04-15 21:17:44 +00:00
mut log: ResMut<Log>,
2024-04-05 00:58:17 +00:00
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_togglemusic: EventWriter<audio::ToggleMusicEvent>,
mut ew_target: EventWriter<TargetEvent>,
mut ew_updateoverlays: EventWriter<UpdateOverlayVisibility>,
q_objects: Query<(Entity, &Transform), (With<IsClickable>, Without<IsTargeted>, Without<actor::PlayerDrivesThis>, Without<actor::Player>)>,
q_camera: Query<&Transform, With<Camera>>,
2024-03-17 18:03:02 +00:00
) {
2024-04-15 21:17:44 +00:00
if keyboard_input.just_pressed(settings.key_help) {
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click));
for line in include_str!("data/keybindings.in").trim().lines().rev() {
2024-04-15 21:17:44 +00:00
log.add(line.to_string(), "".to_string(), LogLevel::Always);
}
}
2024-03-17 18:03:02 +00:00
if keyboard_input.just_pressed(settings.key_togglehud) {
settings.hud_active ^= true;
2024-04-05 00:58:17 +00:00
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch));
ew_togglemusic.send(audio::ToggleMusicEvent());
ew_updateoverlays.send(UpdateOverlayVisibility);
2024-03-17 18:03:02 +00:00
}
2024-04-05 20:43:14 +00:00
if settings.hud_active && mouse_input.just_pressed(settings.key_selectobject) {
2024-04-05 18:38:44 +00:00
if let Ok(camtrans) = q_camera.get_single() {
let objects: Vec<(Entity, &Transform)> = q_objects.iter().collect();
if let (Some(new_target), _dist) = camera::find_closest_target::<Entity>(objects, camtrans) {
ew_target.send(TargetEvent(Some(new_target)));
}
2024-04-05 18:38:44 +00:00
else {
ew_target.send(TargetEvent(None));
}
}
}
2024-03-20 01:03:42 +00:00
}
fn handle_target_event(
mut commands: Commands,
settings: Res<var::Settings>,
mut er_target: EventReader<TargetEvent>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
q_target: Query<Entity, With<IsTargeted>>,
) {
let mut play_sfx = false;
for TargetEvent(target) in er_target.read() {
for old_target in &q_target {
commands.entity(old_target).remove::<IsTargeted>();
play_sfx = true;
}
if let Some(entity) = target {
commands.entity(*entity).insert(IsTargeted);
play_sfx = true;
}
if play_sfx && !settings.mute_sfx {
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click));
}
break; // Only accept a single event per frame
}
}
fn update_target_selectagon(
settings: Res<var::Settings>,
2024-04-05 20:16:01 +00:00
mut q_selectagon: Query<(&mut Transform, &mut Visibility), (With<Selectagon>, Without<IsTargeted>, Without<Camera>)>,
q_target: Query<&Transform, (With<IsTargeted>, Without<Camera>, Without<Selectagon>)>,
q_camera: Query<&Transform, (With<Camera>, Without<IsTargeted>, Without<Selectagon>)>,
) {
2024-04-05 20:43:14 +00:00
if !settings.hud_active || q_camera.is_empty() {
2024-04-05 20:16:01 +00:00
return;
}
let camera_trans = q_camera.get_single().unwrap();
if let Ok((mut selectagon_trans, mut selectagon_vis)) = q_selectagon.get_single_mut() {
if let Ok(target_trans) = q_target.get_single() {
match *selectagon_vis {
Visibility::Hidden => { *selectagon_vis = Visibility::Visible; },
_ => {}
}
selectagon_trans.translation = target_trans.translation;
selectagon_trans.scale = target_trans.scale;
2024-04-24 19:20:28 +00:00
selectagon_trans.look_at(camera_trans.translation, target_trans.up().into());
// Enlarge Selectagon to a minimum angular diameter
let (angular_diameter, _, _) = camera::calc_angular_diameter(
&selectagon_trans, camera_trans);
let min_angular_diameter = 2.0f32.to_radians();
if angular_diameter < min_angular_diameter {
selectagon_trans.scale *= min_angular_diameter / angular_diameter;
}
2024-04-05 20:16:01 +00:00
}
else {
match *selectagon_vis {
Visibility::Hidden => {},
_ => { *selectagon_vis = Visibility::Hidden; }
}
}
}
}
2024-04-10 19:03:30 +00:00
fn update_ar_overlays (
q_owners: Query<(Entity, &Transform, &Visibility), (With<AugmentedRealityOverlayBroadcaster>, Without<AugmentedRealityOverlay>)>,
mut q_overlays: Query<(&mut Transform, &mut Visibility, &mut AugmentedRealityOverlay)>,
settings: ResMut<var::Settings>,
2024-04-10 19:03:30 +00:00
mut state: ResMut<AugmentedRealityState>,
) {
let (need_activate, need_clean, need_update);
if settings.hud_active {
need_activate = !state.overlays_visible;
need_clean = false;
}
else {
need_activate = false;
need_clean = state.overlays_visible;
}
need_update = settings.hud_active;
state.overlays_visible = settings.hud_active;
if need_update || need_clean || need_activate {
for (mut trans, mut vis, ar) in &mut q_overlays {
2024-04-10 19:03:30 +00:00
for (owner_id, owner_trans, owner_vis) in &q_owners {
if owner_id == ar.owner {
*trans = *owner_trans;
if need_clean {
*vis = Visibility::Hidden;
}
else {
*vis = *owner_vis;
}
break;
2024-04-10 19:03:30 +00:00
}
}
}
}
}
fn update_poi_overlays (
mut q_marker: Query<(&mut Transform, &PointOfInterestMarker)>,
q_parent: Query<&Transform, Without<PointOfInterestMarker>>,
q_camera: Query<&Transform, (With<Camera>, Without<PointOfInterestMarker>)>,
settings: ResMut<var::Settings>,
) {
if !settings.hud_active || !settings.map_active || q_camera.is_empty() {
return;
}
let camera_trans = q_camera.get_single().unwrap();
for (mut trans, marker) in &mut q_marker {
if let Ok(parent_trans) = q_parent.get(marker.0) {
// Enlarge POI marker to a minimum angular diameter
trans.translation = parent_trans.translation;
trans.scale = Vec3::splat(1.0);
let (angular_diameter, _, _) = camera::calc_angular_diameter(
&trans, camera_trans);
2024-04-21 19:47:04 +00:00
let min_angular_diameter = 3.0f32.to_radians();
if angular_diameter < min_angular_diameter {
trans.scale *= min_angular_diameter / angular_diameter;
}
trans.look_at(camera_trans.translation, camera_trans.up().into());
}
}
}
fn update_overlay_visibility(
mut q_marker: Query<&mut Visibility, With<PointOfInterestMarker>>,
mut q_hudelement: Query<&mut Visibility, (With<ToggleableHudElement>, Without<PointOfInterestMarker>)>,
2024-04-24 18:02:51 +00:00
mut q_selectagon: Query<&mut Visibility, (With<Selectagon>, Without<ToggleableHudElement>, Without<PointOfInterestMarker>)>,
q_target: Query<&IsTargeted, (Without<Camera>, Without<Selectagon>, Without<PointOfInterestMarker>, Without<ToggleableHudElement>)>,
mut ambient_light: ResMut<AmbientLight>,
er_target: EventReader<UpdateOverlayVisibility>,
settings: Res<var::Settings>,
) {
if er_target.is_empty() {
return;
}
let check = {|check: bool|
if check { Visibility::Inherited } else { Visibility::Hidden }
};
let show_poi = check(settings.hud_active && settings.map_active);
let show_hud = check(settings.hud_active);
2024-04-24 18:02:51 +00:00
let show_selectagon = check(settings.hud_active && !q_target.is_empty());
for mut vis in &mut q_marker {
*vis = show_poi;
}
for mut vis in &mut q_hudelement {
*vis = show_hud;
}
2024-04-24 18:02:51 +00:00
for mut vis in &mut q_selectagon {
*vis = show_selectagon;
}
ambient_light.brightness = if settings.hud_active {
AMBIENT_LIGHT_AR
} else {
AMBIENT_LIGHT
};
}
fn bool2vis(parameter: bool) -> Visibility {
if parameter {
Visibility::Inherited
} else {
Visibility::Hidden
}
}